agora inbox for pgsql-hackers@postgresql.org  
help / color / mirror / Atom feed
[PATCH 5/6] BRIN minmax-multi indexes
12+ messages / 4 participants
[nested] [flat]

* [PATCH 5/6] BRIN minmax-multi indexes
@ 2020-09-05 22:10 Tomas Vondra <tv@fuzzy.cz>
  0 siblings, 0 replies; 12+ messages in thread

From: Tomas Vondra @ 2020-09-05 22:10 UTC (permalink / raw)

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


--2wz2lexbyaxcz4wc
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment;
	filename="0006-tweak-costing-for-bloom-minmax-multi-index-20200911b.patch"



^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* [PATCH 4/5] BRIN minmax-multi indexes
@ 2020-09-12 13:07 Tomas Vondra <tomas.vondra@postgresql.org>
  0 siblings, 0 replies; 12+ messages in thread

From: Tomas Vondra @ 2020-09-12 13:07 UTC (permalink / raw)

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


--c7tltgiw2u7hovas
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment;
	filename="0005-Ignore-correlation-for-new-BRIN-opclasses-20200912.patch"



^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* [PATCH 5/6] BRIN minmax-multi indexes
@ 2020-09-12 13:07 Tomas Vondra <tomas.vondra@postgresql.org>
  0 siblings, 0 replies; 12+ messages in thread

From: Tomas Vondra @ 2020-09-12 13:07 UTC (permalink / raw)

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


--oygwhy5yjes6547y
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment;
	filename="0006-Ignore-correlation-for-new-BRIN-opclasses-20200917.patch"



^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* [PATCH 5/6] BRIN minmax-multi indexes
@ 2020-09-12 13:07 Tomas Vondra <tomas.vondra@postgresql.org>
  0 siblings, 0 replies; 12+ messages in thread

From: Tomas Vondra @ 2020-09-12 13:07 UTC (permalink / raw)

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


--7kiiugxvbzvpnhzt
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment;
	filename="0006-Ignore-correlation-for-new-BRIN-opclasses-20200913.patch"



^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* [PATCH 7/8] BRIN minmax-multi indexes
@ 2020-12-16 20:32 Tomas Vondra <tomas.vondra@postgresql.org>
  0 siblings, 0 replies; 12+ messages in thread

From: Tomas Vondra @ 2020-12-16 20:32 UTC (permalink / raw)

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


--------------45196B023835614BDFCBD16D
Content-Type: text/x-patch; charset=UTF-8;
 name="0006-use-one-hash-bloom-variant-20201220.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename="0006-use-one-hash-bloom-variant-20201220.patch"



^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* [PATCH 5/6] BRIN minmax-multi indexes
@ 2020-12-16 20:32 Tomas Vondra <tomas.vondra@postgresql.org>
  0 siblings, 0 replies; 12+ messages in thread

From: Tomas Vondra @ 2020-12-16 20:32 UTC (permalink / raw)

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


--------------310A2AE1CC4C2E2E77559E3D
Content-Type: text/x-patch; charset=UTF-8;
 name="0006-Ignore-correlation-for-new-BRIN-opclasses-20210114-2.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0006-Ignore-correlation-for-new-BRIN-opclasses-20210114-2.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* [PATCH 6/8] BRIN minmax-multi indexes
@ 2021-02-03 18:00 Tomas Vondra <tomas@2ndquadrant.com>
  0 siblings, 0 replies; 12+ messages in thread

From: Tomas Vondra @ 2021-02-03 18:00 UTC (permalink / raw)

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


--------------D524CD37E77B36A2883A7617
Content-Type: text/x-patch; charset=UTF-8;
 name="0007-Ignore-correlation-for-new-BRIN-opclasses-20210311.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0007-Ignore-correlation-for-new-BRIN-opclasses-20210311.patc";
 filename*1="h"



^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* [PATCH 5/6] BRIN minmax-multi indexes
@ 2021-02-03 18:00 Tomas Vondra <tomas@2ndquadrant.com>
  0 siblings, 0 replies; 12+ messages in thread

From: Tomas Vondra @ 2021-02-03 18:00 UTC (permalink / raw)

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


--------------6A37408A2C42B5B56C3B3D49
Content-Type: text/x-patch; charset=UTF-8;
 name="0006-Ignore-correlation-for-new-BRIN-opclasses-20210303.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0006-Ignore-correlation-for-new-BRIN-opclasses-20210303.patc";
 filename*1="h"



^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* [PATCH 5/9] BRIN minmax-multi indexes
@ 2021-02-03 18:00 Tomas Vondra <tomas@2ndquadrant.com>
  0 siblings, 0 replies; 12+ messages in thread

From: Tomas Vondra @ 2021-02-03 18:00 UTC (permalink / raw)

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


--------------0E72B707603BED22B4040825
Content-Type: text/x-patch; charset=UTF-8;
 name="0004-BRIN-bloom-indexes-20210211.patch"
Content-Transfer-Encoding: 8bit
Content-Disposition: attachment;
 filename="0004-BRIN-bloom-indexes-20210211.patch"



^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* [PATCH 5/9] BRIN minmax-multi indexes
@ 2021-02-03 18:00 Tomas Vondra <tomas@2ndquadrant.com>
  0 siblings, 0 replies; 12+ messages in thread

From: Tomas Vondra @ 2021-02-03 18:00 UTC (permalink / raw)

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


--------------22A4B241170149838D4D1F8F
Content-Type: text/x-patch; charset=UTF-8;
 name="0006-Batch-mode-when-building-new-BRIN-multi-min-20210215.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename*0="0006-Batch-mode-when-building-new-BRIN-multi-min-20210215.pa";
 filename*1="tch"



^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* [PATCH 6/8] BRIN minmax-multi indexes
@ 2021-03-21 23:24 Tomas Vondra <tomas.vondra@postgresql.org>
  0 siblings, 0 replies; 12+ messages in thread

From: Tomas Vondra @ 2021-03-21 23:24 UTC (permalink / raw)

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


--------------76348C5C2C314FA585652FE5
Content-Type: text/x-patch; charset=UTF-8;
 name="0007-move-bloom-to-contrib-20210322.patch"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
 filename="0007-move-bloom-to-contrib-20210322.patch"



^ permalink  raw  reply  [nested|flat] 12+ messages in thread

* [PATCH v45 3/8] Add CONCURRENTLY option to REPACK command.
@ 2026-03-11 14:16 Antonin Houska <ah@cybertec.at>
  0 siblings, 0 replies; 12+ messages in thread

From: Antonin Houska @ 2026-03-11 14:16 UTC (permalink / raw)

The REPACK command copies the relation data into a new file, creates new
indexes and eventually swaps the files. To make sure that the old file does
not change during the copying, the relation is locked in an exclusive mode,
which prevents applications from both reading and writing. (To keep the data
consistent, we'd only need to prevent the applications from writing, but even
reading needs to be blocked before we can swap the files - otherwise some
applications could continue using the old file. Currently, REPACK takes the
simple approach and acquires the exclusive lock in the beginning.

This patch introduces an alternative workflow, which only requires the
exclusive lock when the relation (and index) files are being swapped.
(Supposedly, the swapping should be pretty fast.) On the other hand, when we
copy the data to the new file, we allow applications to read from the relation
and even to write to it.

First, we scan the relation using a "historic snapshot", and insert all the
tuples satisfying this snapshot into the new relation.

Second, logical decoding is used to capture the data changes done by
applications during the copying (i.e. changes not yet committed from the
perspective of the historic snapshot mentioned above), and those are applied
to the new file before we acquire the exclusive lock that we need to swap the
files. (Of course, more data changes can take place while we are waiting for
the lock - these will be applied to the new file after we have acquired the
lock and before we swap the files.)

While the "concurrent data" changes are applied at specific stages (we cannot
do that until the intial copy is finished and indexes are built), a background
worker performs the decoding all the time. This way we minimize the amount of
not-yet-decoded WAL, so that archiving / recycling of WAL segments is not
delayed much. The decoded changes are written to files and passed to the
backed performing REPACK.

Since the logical decoding system, during its startup, waits until all the
transactions which already have XID assigned have finished, there is a risk of
deadlock if a transaction that already changed anything in the database tries
to acquire a conflicting lock on the table REPACK CONCURRENTLY is working
on. As an example, consider transaction running CREATE INDEX command on the
table that is being REPACKed CONCURRENTLY. On the other hand, DML commands
(INSERT, UPDATE, DELETE) are not a problem as their lock does not conflict
with REPACK CONCURRENTLY.

The current approach is that we accept the risk. If we tried to avoid it, it'd
be necessary to unlock the table before the logical decoding is setup and lock
it again afterwards. Such temporary unlocking would imply re-checking if the
table still meets all the requirements for REPACK CONCURRENTLY.

The WAL records produced by running DML commands on the new relation are
intentionally not fed to the logical decoding system. Doing so would introduce
significant overhead, and - as the new relation is never available for logical
replication - it would be useless.
---
 doc/src/sgml/monitoring.sgml                  |   37 +-
 doc/src/sgml/mvcc.sgml                        |   12 +-
 doc/src/sgml/ref/repack.sgml                  |  112 +-
 src/Makefile                                  |    1 +
 src/backend/access/heap/heapam.c              |   41 +-
 src/backend/access/heap/heapam_handler.c      |  221 +-
 src/backend/access/heap/rewriteheap.c         |    6 +-
 src/backend/access/table/tableam.c            |    3 +-
 src/backend/catalog/system_views.sql          |   19 +-
 src/backend/commands/Makefile                 |    1 +
 src/backend/commands/cluster.c                | 1819 ++++++++++++++++-
 src/backend/commands/matview.c                |    1 +
 src/backend/commands/meson.build              |    1 +
 src/backend/commands/repack_worker.c          |  546 +++++
 src/backend/commands/tablecmds.c              |    1 +
 src/backend/commands/vacuum.c                 |   12 +-
 src/backend/executor/nodeModifyTable.c        |   11 +-
 src/backend/libpq/pqmq.c                      |    5 +
 src/backend/meson.build                       |    1 +
 src/backend/postmaster/bgworker.c             |    7 +-
 src/backend/replication/logical/decode.c      |   37 +-
 src/backend/replication/logical/logical.c     |    6 +-
 src/backend/replication/logical/snapbuild.c   |   11 +-
 .../replication/pgoutput_repack/Makefile      |   32 +
 .../replication/pgoutput_repack/meson.build   |   18 +
 .../pgoutput_repack/pgoutput_repack.c         |  289 +++
 src/backend/replication/walsender.c           |    2 +-
 src/backend/storage/ipc/procsignal.c          |    4 +
 .../storage/lmgr/generate-lwlocknames.pl      |    2 +-
 src/backend/tcop/postgres.c                   |    4 +
 .../utils/activity/wait_event_names.txt       |    1 +
 src/backend/utils/time/snapmgr.c              |    3 +-
 src/bin/psql/tab-complete.in.c                |    4 +-
 src/include/access/heapam.h                   |    6 +-
 src/include/access/heapam_xlog.h              |    2 +
 src/include/access/tableam.h                  |   39 +-
 src/include/commands/cluster.h                |   16 +-
 src/include/commands/progress.h               |   17 +-
 src/include/commands/repack_internal.h        |  128 ++
 src/include/replication/decode.h              |    4 +
 src/include/replication/snapbuild.h           |    2 +-
 src/include/storage/lockdefs.h                |    4 +-
 src/include/storage/procsignal.h              |    1 +
 src/include/utils/snapmgr.h                   |    2 +
 src/test/modules/injection_points/Makefile    |    2 +
 .../injection_points/expected/repack.out      |  113 +
 .../expected/repack_toast.out                 |   65 +
 src/test/modules/injection_points/meson.build |    2 +
 .../injection_points/specs/repack.spec        |  142 ++
 .../injection_points/specs/repack_toast.spec  |  112 +
 src/test/regress/expected/rules.out           |   19 +-
 src/tools/pgindent/typedefs.list              |    6 +
 52 files changed, 3660 insertions(+), 292 deletions(-)
 create mode 100644 src/backend/commands/repack_worker.c
 create mode 100644 src/backend/replication/pgoutput_repack/Makefile
 create mode 100644 src/backend/replication/pgoutput_repack/meson.build
 create mode 100644 src/backend/replication/pgoutput_repack/pgoutput_repack.c
 create mode 100644 src/include/commands/repack_internal.h
 create mode 100644 src/test/modules/injection_points/expected/repack.out
 create mode 100644 src/test/modules/injection_points/expected/repack_toast.out
 create mode 100644 src/test/modules/injection_points/specs/repack.spec
 create mode 100644 src/test/modules/injection_points/specs/repack_toast.spec

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index bb75ed1069b..a0a155eb10b 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -6985,14 +6985,35 @@ FROM pg_stat_get_backend_idset() AS backendid;
 
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>heap_tuples_written</structfield> <type>bigint</type>
+       <structfield>heap_tuples_inserted</structfield> <type>bigint</type>
       </para>
       <para>
-       Number of heap tuples written.
+       Number of heap tuples inserted.
        This counter only advances when the phase is
        <literal>seq scanning heap</literal>,
-       <literal>index scanning heap</literal>
-       or <literal>writing new heap</literal>.
+       <literal>index scanning heap</literal>,
+       <literal>writing new heap</literal>
+       or <literal>catch-up</literal>.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>heap_tuples_updated</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of heap tuples updated.
+       This counter only advances when the phase is <literal>catch-up</literal>.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>heap_tuples_deleted</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of heap tuples deleted.
+       This counter only advances when the phase is <literal>catch-up</literal>.
       </para></entry>
      </row>
 
@@ -7073,6 +7094,14 @@ FROM pg_stat_get_backend_idset() AS backendid;
        <command>REPACK</command> is currently writing the new heap.
      </entry>
     </row>
+    <row>
+     <entry><literal>catch-up</literal></entry>
+     <entry>
+       <command>REPACK CONCURRENTLY</command> is currently processing the DML
+       commands that other transactions executed during any of the preceding
+       phases.
+     </entry>
+    </row>
     <row>
      <entry><literal>swapping relation files</literal></entry>
      <entry>
diff --git a/doc/src/sgml/mvcc.sgml b/doc/src/sgml/mvcc.sgml
index e775260936a..241caeb3593 100644
--- a/doc/src/sgml/mvcc.sgml
+++ b/doc/src/sgml/mvcc.sgml
@@ -1845,15 +1845,17 @@ SELECT pg_advisory_lock(q.id) FROM
    <title>Caveats</title>
 
    <para>
-    Some DDL commands, currently only <link linkend="sql-truncate"><command>TRUNCATE</command></link> and the
-    table-rewriting forms of <link linkend="sql-altertable"><command>ALTER TABLE</command></link>, are not
+    Some commands, currently only <link linkend="sql-truncate"><command>TRUNCATE</command></link>, the
+    table-rewriting forms of <link linkend="sql-altertable"><command>ALTER
+    TABLE</command></link> and <command>REPACK</command> with
+    the <literal>CONCURRENTLY</literal> option, are not
     MVCC-safe.  This means that after the truncation or rewrite commits, the
     table will appear empty to concurrent transactions, if they are using a
-    snapshot taken before the DDL command committed.  This will only be an
+    snapshot taken before the command committed.  This will only be an
     issue for a transaction that did not access the table in question
-    before the DDL command started &mdash; any transaction that has done so
+    before the command started &mdash; any transaction that has done so
     would hold at least an <literal>ACCESS SHARE</literal> table lock,
-    which would block the DDL command until that transaction completes.
+    which would block the truncating or rewriting command until that transaction completes.
     So these commands will not cause any apparent inconsistency in the
     table contents for successive queries on the target table, but they
     could cause visible inconsistency between the contents of the target
diff --git a/doc/src/sgml/ref/repack.sgml b/doc/src/sgml/ref/repack.sgml
index 8ccf7c7a417..170eb84bf43 100644
--- a/doc/src/sgml/ref/repack.sgml
+++ b/doc/src/sgml/ref/repack.sgml
@@ -28,6 +28,7 @@ REPACK [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] USING
 
     VERBOSE [ <replaceable class="parameter">boolean</replaceable> ]
     ANALYZE [ <replaceable class="parameter">boolean</replaceable> ]
+    CONCURRENTLY [ <replaceable class="parameter">boolean</replaceable> ]
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
 
@@ -54,7 +55,8 @@ REPACK [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] USING
    processes every table and materialized view in the current database that
    the current user has the <literal>MAINTAIN</literal> privilege on. This
    form of <command>REPACK</command> cannot be executed inside a transaction
-   block.
+   block.  Also, this form is not allowed if
+   the <literal>CONCURRENTLY</literal> option is used.
   </para>
 
   <para>
@@ -67,7 +69,8 @@ REPACK [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] USING
    When a table is being repacked, an <literal>ACCESS EXCLUSIVE</literal> lock
    is acquired on it. This prevents any other database operations (both reads
    and writes) from operating on the table until the <command>REPACK</command>
-   is finished.
+   is finished. If you want to keep the table accessible during the repacking,
+   consider using the <literal>CONCURRENTLY</literal> option.
   </para>
 
   <refsect2 id="sql-repack-notes-on-clustering" xreflabel="Notes on Clustering">
@@ -198,6 +201,111 @@ REPACK [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] USING
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>CONCURRENTLY</literal></term>
+    <listitem>
+     <para>
+      Allow other transactions to use the table while it is being repacked.
+     </para>
+
+     <para>
+      Internally, <command>REPACK</command> copies the contents of the table
+      (ignoring dead tuples) into a new file, sorted by the specified index,
+      and also creates a new file for each index. Then it swaps the old and
+      new files for the table and all the indexes, and deletes the old
+      files. The <literal>ACCESS EXCLUSIVE</literal> lock is needed to make
+      sure that the old files do not change during the processing because the
+      changes would get lost due to the swap.
+     </para>
+
+     <para>
+      With the <literal>CONCURRENTLY</literal> option, the <literal>ACCESS
+      EXCLUSIVE</literal> lock is only acquired to swap the table and index
+      files. The data changes that took place during the creation of the new
+      table and index files are captured using logical decoding
+      (<xref linkend="logicaldecoding"/>) and applied before
+      the <literal>ACCESS EXCLUSIVE</literal> lock is requested. Thus the lock
+      is typically held only for the time needed to swap the files, which
+      should be pretty short. However, the time might still be noticeable if
+      too many data changes have been done to the table while
+      <command>REPACK</command> was waiting for the lock: those changes must
+      be processed just before the files are swapped, while the
+      <literal>ACCESS EXCLUSIVE</literal> lock is being held.
+     </para>
+
+     <para>
+      Note that <command>REPACK</command> with the
+      <literal>CONCURRENTLY</literal> option does not try to order the rows
+      inserted into the table after the repacking started. Also
+      note <command>REPACK</command> might fail to complete due to DDL
+      commands executed on the table by other transactions during the
+      repacking.
+     </para>
+
+     <note>
+      <para>
+       In addition to the temporary space requirements explained in
+       <xref linkend="sql-repack-notes-on-resources"/>,
+       the <literal>CONCURRENTLY</literal> option can add to the usage of
+       temporary space a bit more. The reason is that other transactions can
+       perform DML operations which cannot be applied to the new file until
+       <command>REPACK</command> has copied all the existing tuples from the
+       old file. Thus the tuples inserted into the old file during the copying
+       are also stored separately in a temporary file, until they can be
+       processed.
+      </para>
+     </note>
+
+     <para>
+      The <literal>CONCURRENTLY</literal> option cannot be used in the
+      following cases:
+
+      <itemizedlist>
+       <listitem>
+        <para>
+          The table is <literal>UNLOGGED</literal>.
+        </para>
+       </listitem>
+
+       <listitem>
+        <para>
+          The table is partitioned.
+        </para>
+       </listitem>
+
+       <listitem>
+        <para>
+          The table is a system catalog or a <acronym>TOAST</acronym> table.
+        </para>
+       </listitem>
+
+       <listitem>
+        <para>
+         <command>REPACK</command> is executed inside a transaction block.
+        </para>
+       </listitem>
+
+       <listitem>
+        <para>
+         The <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+         configuration parameter does not allow for creation of an additional
+         replication slot.
+        </para>
+       </listitem>
+      </itemizedlist>
+     </para>
+
+     <warning>
+      <para>
+       <command>REPACK</command> with the <literal>CONCURRENTLY</literal>
+       option is not MVCC-safe, see <xref linkend="mvcc-caveats"/> for
+       details.
+      </para>
+     </warning>
+
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>VERBOSE</literal></term>
     <listitem>
diff --git a/src/Makefile b/src/Makefile
index 2f31a2f20a7..b18c9a14ffa 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -23,6 +23,7 @@ SUBDIRS = \
 	interfaces \
 	backend/replication/libpqwalreceiver \
 	backend/replication/pgoutput \
+	backend/replication/pgoutput_repack \
 	fe_utils \
 	bin \
 	pl \
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 044f385e477..52b42280959 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -61,7 +61,8 @@ static HeapTuple heap_prepare_insert(Relation relation, HeapTuple tup,
 static XLogRecPtr log_heap_update(Relation reln, Buffer oldbuf,
 								  Buffer newbuf, HeapTuple oldtup,
 								  HeapTuple newtup, HeapTuple old_key_tuple,
-								  bool all_visible_cleared, bool new_all_visible_cleared);
+								  bool all_visible_cleared, bool new_all_visible_cleared,
+								  bool walLogical);
 #ifdef USE_ASSERT_CHECKING
 static void check_lock_if_inplace_updateable_rel(Relation relation,
 												 const ItemPointerData *otid,
@@ -2852,8 +2853,8 @@ xmax_infomask_changed(uint16 new_infomask, uint16 old_infomask)
  */
 TM_Result
 heap_delete(Relation relation, const ItemPointerData *tid,
-			CommandId cid, Snapshot crosscheck, bool wait,
-			TM_FailureData *tmfd, bool changingPart)
+			CommandId cid, Snapshot crosscheck, int options,
+			TM_FailureData *tmfd)
 {
 	TM_Result	result;
 	TransactionId xid = GetCurrentTransactionId();
@@ -2863,6 +2864,9 @@ heap_delete(Relation relation, const ItemPointerData *tid,
 	BlockNumber block;
 	Buffer		buffer;
 	Buffer		vmbuffer = InvalidBuffer;
+	bool		wait = (options & TABLE_DELETE_WAIT) != 0;
+	bool		changingPart = (options & TABLE_DELETE_CHANGING_PART) != 0;
+	bool		walLogical = (options & TABLE_DELETE_NO_LOGICAL) == 0;
 	TransactionId new_xmax;
 	uint16		new_infomask,
 				new_infomask2;
@@ -3100,7 +3104,8 @@ l1:
 	 * Compute replica identity tuple before entering the critical section so
 	 * we don't PANIC upon a memory allocation failure.
 	 */
-	old_key_tuple = ExtractReplicaIdentity(relation, &tp, true, &old_key_copied);
+	old_key_tuple = walLogical ?
+		ExtractReplicaIdentity(relation, &tp, true, &old_key_copied) : NULL;
 
 	/*
 	 * If this is the first possibly-multixact-able operation in the current
@@ -3190,6 +3195,15 @@ l1:
 				xlrec.flags |= XLH_DELETE_CONTAINS_OLD_KEY;
 		}
 
+		/*
+		 * Unlike UPDATE, DELETE is decoded even if there is no old key, so it
+		 * does not help to clear both XLH_DELETE_CONTAINS_OLD_TUPLE and
+		 * XLH_DELETE_CONTAINS_OLD_KEY. Thus we need an extra flag. TODO
+		 * Consider not decoding tuples w/o the old tuple/key instead.
+		 */
+		if (!walLogical)
+			xlrec.flags |= XLH_DELETE_NO_LOGICAL;
+
 		XLogBeginInsert();
 		XLogRegisterData(&xlrec, SizeOfHeapDelete);
 
@@ -3281,8 +3295,8 @@ simple_heap_delete(Relation relation, const ItemPointerData *tid)
 
 	result = heap_delete(relation, tid,
 						 GetCurrentCommandId(true), InvalidSnapshot,
-						 true /* wait for commit */ ,
-						 &tmfd, false /* changingPart */ );
+						 TABLE_UPDATE_WAIT,
+						 &tmfd);
 	switch (result)
 	{
 		case TM_SelfModified:
@@ -3321,7 +3335,7 @@ simple_heap_delete(Relation relation, const ItemPointerData *tid)
  */
 TM_Result
 heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup,
-			CommandId cid, Snapshot crosscheck, bool wait,
+			CommandId cid, Snapshot crosscheck, int options,
 			TM_FailureData *tmfd, LockTupleMode *lockmode,
 			TU_UpdateIndexes *update_indexes)
 {
@@ -3338,6 +3352,8 @@ heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup,
 	HeapTuple	heaptup;
 	HeapTuple	old_key_tuple = NULL;
 	bool		old_key_copied = false;
+	bool		wait = (options & TABLE_UPDATE_WAIT) != 0;
+	bool		walLogical = (options & TABLE_UPDATE_NO_LOGICAL) == 0;
 	Page		page,
 				newpage;
 	BlockNumber block;
@@ -4219,7 +4235,8 @@ l2:
 								 newbuf, &oldtup, heaptup,
 								 old_key_tuple,
 								 all_visible_cleared,
-								 all_visible_cleared_new);
+								 all_visible_cleared_new,
+								 walLogical);
 		if (newbuf != buffer)
 		{
 			PageSetLSN(newpage, recptr);
@@ -4576,7 +4593,7 @@ simple_heap_update(Relation relation, const ItemPointerData *otid, HeapTuple tup
 
 	result = heap_update(relation, otid, tup,
 						 GetCurrentCommandId(true), InvalidSnapshot,
-						 true /* wait for commit */ ,
+						 TABLE_UPDATE_WAIT,
 						 &tmfd, &lockmode, update_indexes);
 	switch (result)
 	{
@@ -8894,7 +8911,8 @@ static XLogRecPtr
 log_heap_update(Relation reln, Buffer oldbuf,
 				Buffer newbuf, HeapTuple oldtup, HeapTuple newtup,
 				HeapTuple old_key_tuple,
-				bool all_visible_cleared, bool new_all_visible_cleared)
+				bool all_visible_cleared, bool new_all_visible_cleared,
+				bool walLogical)
 {
 	xl_heap_update xlrec;
 	xl_heap_header xlhdr;
@@ -8905,7 +8923,8 @@ log_heap_update(Relation reln, Buffer oldbuf,
 				suffixlen = 0;
 	XLogRecPtr	recptr;
 	Page		page = BufferGetPage(newbuf);
-	bool		need_tuple_data = RelationIsLogicallyLogged(reln);
+	bool		need_tuple_data = RelationIsLogicallyLogged(reln) &&
+		walLogical;
 	bool		init;
 	int			bufflags;
 
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index d40878928e1..0ba7c0df1fc 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -311,22 +311,22 @@ heapam_tuple_complete_speculative(Relation relation, TupleTableSlot *slot,
 
 static TM_Result
 heapam_tuple_delete(Relation relation, ItemPointer tid, CommandId cid,
-					Snapshot snapshot, Snapshot crosscheck, bool wait,
-					TM_FailureData *tmfd, bool changingPart)
+					Snapshot snapshot, Snapshot crosscheck, int options,
+					TM_FailureData *tmfd)
 {
 	/*
 	 * Currently Deleting of index tuples are handled at vacuum, in case if
 	 * the storage itself is cleaning the dead tuples by itself, it is the
 	 * time to call the index tuple deletion also.
 	 */
-	return heap_delete(relation, tid, cid, crosscheck, wait, tmfd, changingPart);
+	return heap_delete(relation, tid, cid, crosscheck, options, tmfd);
 }
 
 
 static TM_Result
 heapam_tuple_update(Relation relation, ItemPointer otid, TupleTableSlot *slot,
 					CommandId cid, Snapshot snapshot, Snapshot crosscheck,
-					bool wait, TM_FailureData *tmfd,
+					int options, TM_FailureData *tmfd,
 					LockTupleMode *lockmode, TU_UpdateIndexes *update_indexes)
 {
 	bool		shouldFree = true;
@@ -337,7 +337,7 @@ heapam_tuple_update(Relation relation, ItemPointer otid, TupleTableSlot *slot,
 	slot->tts_tableOid = RelationGetRelid(relation);
 	tuple->t_tableOid = slot->tts_tableOid;
 
-	result = heap_update(relation, otid, tuple, cid, crosscheck, wait,
+	result = heap_update(relation, otid, tuple, cid, crosscheck, options,
 						 tmfd, lockmode, update_indexes);
 	ItemPointerCopy(&tuple->t_self, &slot->tts_tid);
 
@@ -695,13 +695,14 @@ static void
 heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 								 Relation OldIndex, bool use_sort,
 								 TransactionId OldestXmin,
+								 Snapshot snapshot,
 								 TransactionId *xid_cutoff,
 								 MultiXactId *multi_cutoff,
 								 double *num_tuples,
 								 double *tups_vacuumed,
 								 double *tups_recently_dead)
 {
-	RewriteState rwstate;
+	RewriteState rwstate = NULL;
 	IndexScanDesc indexScan;
 	TableScanDesc tableScan;
 	HeapScanDesc heapScan;
@@ -715,6 +716,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 	bool	   *isnull;
 	BufferHeapTupleTableSlot *hslot;
 	BlockNumber prev_cblock = InvalidBlockNumber;
+	bool		concurrent = snapshot != NULL;
 
 	/* Remember if it's a system catalog */
 	is_system_catalog = IsSystemRelation(OldHeap);
@@ -730,9 +732,12 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 	values = palloc_array(Datum, natts);
 	isnull = palloc_array(bool, natts);
 
-	/* Initialize the rewrite operation */
-	rwstate = begin_heap_rewrite(OldHeap, NewHeap, OldestXmin, *xid_cutoff,
-								 *multi_cutoff);
+	/*
+	 * Initialize the rewrite operation.
+	 */
+	if (!concurrent)
+		rwstate = begin_heap_rewrite(OldHeap, NewHeap, OldestXmin,
+									 *xid_cutoff, *multi_cutoff);
 
 
 	/* Set up sorting if wanted */
@@ -747,6 +752,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 	 * Prepare to scan the OldHeap.  To ensure we see recently-dead tuples
 	 * that still need to be copied, we scan with SnapshotAny and use
 	 * HeapTupleSatisfiesVacuum for the visibility test.
+	 *
+	 * In the CONCURRENTLY case, we do regular MVCC visibility tests, using
+	 * the snapshot passed by the caller.
 	 */
 	if (OldIndex != NULL && !use_sort)
 	{
@@ -763,7 +771,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 
 		tableScan = NULL;
 		heapScan = NULL;
-		indexScan = index_beginscan(OldHeap, OldIndex, SnapshotAny, NULL, 0, 0);
+		indexScan = index_beginscan(OldHeap, OldIndex,
+									snapshot ? snapshot : SnapshotAny,
+									NULL, 0, 0);
 		index_rescan(indexScan, NULL, 0, NULL, 0);
 	}
 	else
@@ -772,7 +782,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 		pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
 									 PROGRESS_REPACK_PHASE_SEQ_SCAN_HEAP);
 
-		tableScan = table_beginscan(OldHeap, SnapshotAny, 0, (ScanKey) NULL);
+		tableScan = table_beginscan(OldHeap,
+									snapshot ? snapshot : SnapshotAny,
+									0, (ScanKey) NULL);
 		heapScan = (HeapScanDesc) tableScan;
 		indexScan = NULL;
 
@@ -848,83 +860,91 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 		buf = hslot->buffer;
 
 		/*
-		 * To be able to guarantee that we can set the hint bit, acquire an
-		 * exclusive lock on the old buffer. We need the hint bits, set in
-		 * heapam_relation_copy_for_cluster() -> HeapTupleSatisfiesVacuum(),
-		 * to be set, as otherwise reform_and_rewrite_tuple() ->
-		 * rewrite_heap_tuple() will get confused. Specifically,
-		 * rewrite_heap_tuple() checks for HEAP_XMAX_INVALID in the old tuple
-		 * to determine whether to check the old-to-new mapping hash table.
-		 *
-		 * It'd be better if we somehow could avoid setting hint bits on the
-		 * old page. One reason to use VACUUM FULL are very bloated tables -
-		 * rewriting most of the old table during VACUUM FULL doesn't exactly
-		 * help...
+		 * Regarding CONCURRENTLY, see the comments on MVCC snapshot above.
 		 */
-		LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
-
-		switch (HeapTupleSatisfiesVacuum(tuple, OldestXmin, buf))
+		if (!concurrent)
 		{
-			case HEAPTUPLE_DEAD:
-				/* Definitely dead */
-				isdead = true;
-				break;
-			case HEAPTUPLE_RECENTLY_DEAD:
-				*tups_recently_dead += 1;
-				pg_fallthrough;
-			case HEAPTUPLE_LIVE:
-				/* Live or recently dead, must copy it */
-				isdead = false;
-				break;
-			case HEAPTUPLE_INSERT_IN_PROGRESS:
+			/*
+			 * To be able to guarantee that we can set the hint bit, acquire
+			 * an exclusive lock on the old buffer. We need the hint bits, set
+			 * in heapam_relation_copy_for_cluster() ->
+			 * HeapTupleSatisfiesVacuum(), to be set, as otherwise
+			 * reform_and_rewrite_tuple() -> rewrite_heap_tuple() will get
+			 * confused. Specifically, rewrite_heap_tuple() checks for
+			 * HEAP_XMAX_INVALID in the old tuple to determine whether to
+			 * check the old-to-new mapping hash table.
+			 *
+			 * It'd be better if we somehow could avoid setting hint bits on
+			 * the old page. One reason to use VACUUM FULL are very bloated
+			 * tables - rewriting most of the old table during VACUUM FULL
+			 * doesn't exactly help...
+			 */
+			LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
 
-				/*
-				 * Since we hold exclusive lock on the relation, normally the
-				 * only way to see this is if it was inserted earlier in our
-				 * own transaction.  However, it can happen in system
-				 * catalogs, since we tend to release write lock before commit
-				 * there.  Give a warning if neither case applies; but in any
-				 * case we had better copy it.
-				 */
-				if (!is_system_catalog &&
-					!TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(tuple->t_data)))
-					elog(WARNING, "concurrent insert in progress within table \"%s\"",
-						 RelationGetRelationName(OldHeap));
-				/* treat as live */
-				isdead = false;
-				break;
-			case HEAPTUPLE_DELETE_IN_PROGRESS:
-
-				/*
-				 * Similar situation to INSERT_IN_PROGRESS case.
-				 */
-				if (!is_system_catalog &&
-					!TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetUpdateXid(tuple->t_data)))
-					elog(WARNING, "concurrent delete in progress within table \"%s\"",
-						 RelationGetRelationName(OldHeap));
-				/* treat as recently dead */
-				*tups_recently_dead += 1;
-				isdead = false;
-				break;
-			default:
-				elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result");
-				isdead = false; /* keep compiler quiet */
-				break;
-		}
-
-		LockBuffer(buf, BUFFER_LOCK_UNLOCK);
-
-		if (isdead)
-		{
-			*tups_vacuumed += 1;
-			/* heap rewrite module still needs to see it... */
-			if (rewrite_heap_dead_tuple(rwstate, tuple))
+			switch (HeapTupleSatisfiesVacuum(tuple, OldestXmin, buf))
 			{
-				/* A previous recently-dead tuple is now known dead */
-				*tups_vacuumed += 1;
-				*tups_recently_dead -= 1;
+				case HEAPTUPLE_DEAD:
+					/* Definitely dead */
+					isdead = true;
+					break;
+				case HEAPTUPLE_RECENTLY_DEAD:
+					*tups_recently_dead += 1;
+					pg_fallthrough;
+				case HEAPTUPLE_LIVE:
+					/* Live or recently dead, must copy it */
+					isdead = false;
+					break;
+				case HEAPTUPLE_INSERT_IN_PROGRESS:
+
+					/*
+					 * As long as we hold exclusive lock on the relation,
+					 * normally the only way to see this is if it was inserted
+					 * earlier in our own transaction.  However, it can happen
+					 * in system catalogs, since we tend to release write lock
+					 * before commit there. Give a warning if neither case
+					 * applies; but in any case we had better copy it.
+					 */
+					if (!is_system_catalog &&
+						!TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(tuple->t_data)))
+						elog(WARNING, "concurrent insert in progress within table \"%s\"",
+							 RelationGetRelationName(OldHeap));
+					/* treat as live */
+					isdead = false;
+					break;
+				case HEAPTUPLE_DELETE_IN_PROGRESS:
+
+					/*
+					 * Similar situation to INSERT_IN_PROGRESS case.
+					 */
+					if (!is_system_catalog &&
+						!TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetUpdateXid(tuple->t_data)))
+						elog(WARNING, "concurrent delete in progress within table \"%s\"",
+							 RelationGetRelationName(OldHeap));
+					/* treat as recently dead */
+					*tups_recently_dead += 1;
+					isdead = false;
+					break;
+				default:
+					elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result");
+					isdead = false; /* keep compiler quiet */
+					break;
+			}
+
+			LockBuffer(buf, BUFFER_LOCK_UNLOCK);
+
+			if (isdead)
+			{
+				*tups_vacuumed += 1;
+				/* heap rewrite module still needs to see it... */
+				if (rewrite_heap_dead_tuple(rwstate, tuple))
+				{
+					/* A previous recently-dead tuple is now known dead */
+					*tups_vacuumed += 1;
+					*tups_recently_dead -= 1;
+				}
+
+				continue;
 			}
-			continue;
 		}
 
 		*num_tuples += 1;
@@ -943,7 +963,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 		{
 			const int	ct_index[] = {
 				PROGRESS_REPACK_HEAP_TUPLES_SCANNED,
-				PROGRESS_REPACK_HEAP_TUPLES_WRITTEN
+				PROGRESS_REPACK_HEAP_TUPLES_INSERTED
 			};
 			int64		ct_val[2];
 
@@ -1001,7 +1021,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 									 values, isnull,
 									 rwstate);
 			/* Report n_tuples */
-			pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_WRITTEN,
+			pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
 										 n_tuples);
 		}
 
@@ -1009,7 +1029,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 	}
 
 	/* Write out any remaining tuples, and fsync if needed */
-	end_heap_rewrite(rwstate);
+	if (rwstate)
+		end_heap_rewrite(rwstate);
 
 	/* Clean up */
 	pfree(values);
@@ -2402,6 +2423,10 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate,
  * SET WITHOUT OIDS.
  *
  * So, we must reconstruct the tuple from component Datums.
+ *
+ * If rwstate=NULL, use simple_heap_insert() instead of rewriting - in that
+ * case we still need to deform/form the tuple. TODO Shouldn't we rename the
+ * function, as might not do any rewrite?
  */
 static void
 reform_and_rewrite_tuple(HeapTuple tuple,
@@ -2424,8 +2449,28 @@ reform_and_rewrite_tuple(HeapTuple tuple,
 
 	copiedTuple = heap_form_tuple(newTupDesc, values, isnull);
 
-	/* The heap rewrite module does the rest */
-	rewrite_heap_tuple(rwstate, tuple, copiedTuple);
+	if (rwstate)
+		/* The heap rewrite module does the rest */
+		rewrite_heap_tuple(rwstate, tuple, copiedTuple);
+	else
+	{
+		/*
+		 * Insert tuple when processing REPACK CONCURRENTLY.
+		 *
+		 * rewriteheap.c is not used in the CONCURRENTLY case because it'd be
+		 * difficult to do the same in the catch-up phase (as the logical
+		 * decoding does not provide us with sufficient visibility
+		 * information). Thus we must use heap_insert() both during the
+		 * catch-up and here.
+		 *
+		 * The following is like simple_heap_insert() except that we pass the
+		 * flag to skip logical decoding: as soon as REPACK CONCURRENTLY swaps
+		 * the relation files, it drops this relation, so no logical
+		 * replication subscription should need the data.
+		 */
+		heap_insert(NewHeap, copiedTuple, GetCurrentCommandId(true),
+					HEAP_INSERT_NO_LOGICAL, NULL);
+	}
 
 	heap_freetuple(copiedTuple);
 }
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index 6b19ac3030d..d706856e7a5 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -621,9 +621,9 @@ raw_heap_insert(RewriteState state, HeapTuple tup)
 		int			options = HEAP_INSERT_SKIP_FSM;
 
 		/*
-		 * While rewriting the heap for VACUUM FULL / CLUSTER, make sure data
-		 * for the TOAST table are not logically decoded.  The main heap is
-		 * WAL-logged as XLOG FPI records, which are not logically decoded.
+		 * While rewriting the heap for REPACK, make sure data for the TOAST
+		 * table are not logically decoded.  The main heap is WAL-logged as
+		 * XLOG FPI records, which are not logically decoded.
 		 */
 		options |= HEAP_INSERT_NO_LOGICAL;
 
diff --git a/src/backend/access/table/tableam.c b/src/backend/access/table/tableam.c
index dfda1af412e..4c759f29b3e 100644
--- a/src/backend/access/table/tableam.c
+++ b/src/backend/access/table/tableam.c
@@ -319,8 +319,7 @@ simple_table_tuple_delete(Relation rel, ItemPointer tid, Snapshot snapshot)
 	result = table_tuple_delete(rel, tid,
 								GetCurrentCommandId(true),
 								snapshot, InvalidSnapshot,
-								true /* wait for commit */ ,
-								&tmfd, false /* changingPart */ );
+								TABLE_DELETE_WAIT, &tmfd);
 
 	switch (result)
 	{
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..75f97cb23e3 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1343,16 +1343,19 @@ CREATE VIEW pg_stat_progress_repack AS
                       WHEN 2 THEN 'index scanning heap'
                       WHEN 3 THEN 'sorting tuples'
                       WHEN 4 THEN 'writing new heap'
-                      WHEN 5 THEN 'swapping relation files'
-                      WHEN 6 THEN 'rebuilding index'
-                      WHEN 7 THEN 'performing final cleanup'
+                      WHEN 5 THEN 'catch-up'
+                      WHEN 6 THEN 'swapping relation files'
+                      WHEN 7 THEN 'rebuilding index'
+                      WHEN 8 THEN 'performing final cleanup'
                       END AS phase,
         CAST(S.param3 AS oid) AS repack_index_relid,
         S.param4 AS heap_tuples_scanned,
-        S.param5 AS heap_tuples_written,
-        S.param6 AS heap_blks_total,
-        S.param7 AS heap_blks_scanned,
-        S.param8 AS index_rebuild_count
+        S.param5 AS heap_tuples_inserted,
+        S.param6 AS heap_tuples_updated,
+        S.param7 AS heap_tuples_deleted,
+        S.param8 AS heap_blks_total,
+        S.param9 AS heap_blks_scanned,
+        S.param10 AS index_rebuild_count
     FROM pg_stat_get_progress_info('REPACK') AS S
         LEFT JOIN pg_database D ON S.datid = D.oid;
 
@@ -1370,7 +1373,7 @@ CREATE VIEW pg_stat_progress_cluster AS
         phase,
         repack_index_relid AS cluster_index_relid,
         heap_tuples_scanned,
-        heap_tuples_written,
+        heap_tuples_inserted + heap_tuples_updated AS heap_tuples_written,
         heap_blks_total,
         heap_blks_scanned,
         index_rebuild_count
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index c10fdba2bbb..6926bc44818 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -51,6 +51,7 @@ OBJS = \
 	proclang.o \
 	propgraphcmds.o \
 	publicationcmds.o \
+	repack_worker.o \
 	schemacmds.o \
 	seclabel.o \
 	sequence.o \
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 09066db0956..65b79ee18ff 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -4,6 +4,22 @@
  *    REPACK a table; formerly known as CLUSTER.  VACUUM FULL also uses
  *    parts of this code.
  *
+ * There are two somewhat different ways to rewrite a table.  In non-
+ * concurrent mode, it's easy: take AccessExclusiveLock, create a new
+ * transient relation, copy the tuples over to the relfilenode of the new
+ * relation, swap the relfilenodes, then drop the old relation.
+ *
+ * In concurrent mode, we lock the table with only ShareUpdateExclusiveLock,
+ * then do an initial copy as above.  However, while the tuples are being
+ * copied, concurrent transactions could modify the table. To cope with those
+ * changes, we rely on logical decoding to obtain them from WAL.  A bgworker
+ * consumes WAL while the initial copy is ongoing (to prevent excessive WAL
+ * from being reserved), and accumulates the changes in a file.  Once the
+ * initial copy is complete, we read the changes from the file and re-apply
+ * them on the new heap.  Then we upgrade our ShareUpdateExclusiveLock to
+ * AccessExclusiveLock and swap the relfilenodes.  This way, the time we hold
+ * a strong lock on the table is much reduced, and the bloat is eliminated.
+ *
  *
  * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994-5, Regents of the University of California
@@ -17,6 +33,7 @@
 #include "postgres.h"
 
 #include "access/amapi.h"
+#include "access/detoast.h"
 #include "access/heapam.h"
 #include "access/multixact.h"
 #include "access/relscan.h"
@@ -24,6 +41,7 @@
 #include "access/toast_internals.h"
 #include "access/transam.h"
 #include "access/xact.h"
+#include "access/xloginsert.h"
 #include "catalog/catalog.h"
 #include "catalog/dependency.h"
 #include "catalog/heap.h"
@@ -31,22 +49,29 @@
 #include "catalog/namespace.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
+#include "catalog/pg_control.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/cluster.h"
 #include "commands/defrem.h"
 #include "commands/progress.h"
+#include "commands/repack_internal.h"
 #include "commands/tablecmds.h"
 #include "commands/vacuum.h"
+#include "executor/executor.h"
+#include "libpq/pqformat.h"
+#include "libpq/pqmq.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
 #include "pgstat.h"
 #include "storage/bufmgr.h"
 #include "storage/lmgr.h"
 #include "storage/predicate.h"
+#include "storage/proc.h"
 #include "utils/acl.h"
 #include "utils/fmgroids.h"
 #include "utils/guc.h"
+#include "utils/injection_point.h"
 #include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
@@ -54,6 +79,7 @@
 #include "utils/relmapper.h"
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
+#include "utils/wait_event_types.h"
 
 /*
  * This struct is used to pass around the information on tables to be
@@ -66,12 +92,79 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * The first file exported by the decoding worker must contain a snapshot, the
+ * following ones contain the data changes.
+ */
+#define WORKER_FILE_SNAPSHOT	0
+
+/*
+ * Information needed to apply concurrent data changes.
+ */
+typedef struct ChangeContext
+{
+	/* The relation the changes are applied to. */
+	Relation	cc_rel;
+
+	/* Needed to update indexes of rel_dst. */
+	ResultRelInfo *cc_rri;
+	EState	   *cc_estate;
+
+	/*
+	 * Existing tuples to UPDATE and DELETE are located via this index. We
+	 * keep the scankey in partially initialized state to avoid repeated work.
+	 * sk_argument is completed on the fly.
+	 */
+	Relation	cc_ident_index;
+	ScanKey		cc_ident_key;
+	int			cc_ident_key_nentries;
+
+	/* Sequential number of the file containing the changes. */
+	int			cc_file_seq;
+} ChangeContext;
+
+/* FIXME probably in repack_worker.h */
+pid_t backend_pid;
+ProcNumber backend_proc_number;
+
+/*
+ * Backend-local information to control the decoding worker.
+ */
+typedef struct DecodingWorker
+{
+	/* The worker. */
+	BackgroundWorkerHandle *handle;
+
+	/* DecodingWorkerShared is in this segment. */
+	dsm_segment *seg;
+
+	/* Handle of the error queue. */
+	shm_mq_handle *error_mqh;
+} DecodingWorker;
+
+/* Pointer to currently running decoding worker. */
+static DecodingWorker *decoding_worker = NULL;
+
+/*
+ * Is there a message sent by a repack worker that the backend needs to
+ * receive?
+ */
+volatile sig_atomic_t RepackMessagePending = false;
+
+static LOCKMODE RepackLockLevel(bool concurrent);
 static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
-								Oid indexOid, Oid userid, int options);
-static void rebuild_relation(Relation OldHeap, Relation index, bool verbose);
+								Oid indexOid, Oid userid, LOCKMODE lmode,
+								int options);
+static void check_repack_concurrently_requirements(Relation rel,
+												   Oid *ident_idx_p);
+static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
+							 Oid ident_idx);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
-							bool verbose, bool *pSwapToastByContent,
-							TransactionId *pFreezeXid, MultiXactId *pCutoffMulti);
+							Snapshot snapshot,
+							bool verbose,
+							bool *pSwapToastByContent,
+							TransactionId *pFreezeXid,
+							MultiXactId *pCutoffMulti);
 static List *get_tables_to_repack(RepackCommand cmd, bool usingindex,
 								  MemoryContext permcxt);
 static List *get_tables_to_repack_partitioned(RepackCommand cmd,
@@ -79,10 +172,45 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
 											  MemoryContext permcxt);
 static bool repack_is_permitted_for_relation(RepackCommand cmd,
 											 Oid relid, Oid userid);
+
+static void apply_concurrent_changes(BufFile *file, ChangeContext *chgcxt);
+static void apply_concurrent_insert(Relation rel, TupleTableSlot *slot,
+									ChangeContext *chgcxt);
+static void apply_concurrent_update(Relation rel, TupleTableSlot *spilled_tuple,
+									TupleTableSlot *ondisk_tuple,
+									ChangeContext *chgcxt);
+static void apply_concurrent_delete(Relation rel, TupleTableSlot *slot);
+static void restore_tuple(BufFile *file, Relation relation,
+						  TupleTableSlot *slot);
+static void adjust_toast_pointers(Relation relation, TupleTableSlot *dest,
+								  TupleTableSlot *src);
+static bool find_target_tuple(Relation rel, ChangeContext *chgcxt,
+							  TupleTableSlot *locator,
+							  TupleTableSlot *received);
+static void process_concurrent_changes(XLogRecPtr end_of_wal,
+									   ChangeContext *chgcxt,
+									   bool done);
+static void initialize_change_context(ChangeContext *chgcxt,
+									  Relation relation,
+									  Oid ident_index_id);
+static void release_change_context(ChangeContext *chgcxt);
+static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
+											   Oid identIdx,
+											   TransactionId frozenXid,
+											   MultiXactId cutoffMulti);
+static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
 static Relation process_single_relation(RepackStmt *stmt,
+										LOCKMODE lockmode,
+										bool isTopLevel,
 										ClusterParams *params);
 static Oid	determine_clustered_index(Relation rel, bool usingindex,
 									  const char *indexname);
+
+static void start_repack_decoding_worker(Oid relid);
+static void stop_repack_decoding_worker(void);
+static Snapshot get_initial_snapshot(DecodingWorker *worker);
+
+static void ProcessRepackMessage(StringInfo msg);
 static const char *RepackCommandAsString(RepackCommand cmd);
 
 
@@ -115,6 +243,7 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	ClusterParams params = {0};
 	Relation	rel = NULL;
 	MemoryContext repack_context;
+	LOCKMODE	lockmode;
 	List	   *rtcs;
 
 	/* Parse option list */
@@ -125,6 +254,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 		else if (strcmp(opt->defname, "analyze") == 0 ||
 				 strcmp(opt->defname, "analyse") == 0)
 			params.options |= defGetBoolean(opt) ? CLUOPT_ANALYZE : 0;
+		else if (strcmp(opt->defname, "concurrently") == 0 &&
+				 defGetBoolean(opt))
+		{
+			if (stmt->command != REPACK_COMMAND_REPACK)
+				ereport(ERROR,
+						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						errmsg("CONCURRENTLY option not supported for %s",
+							   RepackCommandAsString(stmt->command)));
+			params.options |= CLUOPT_CONCURRENT;
+		}
 		else
 			ereport(ERROR,
 					errcode(ERRCODE_SYNTAX_ERROR),
@@ -134,13 +273,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 					parser_errposition(pstate, opt->location));
 	}
 
+	/* Determine the lock mode to use. */
+	lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0);
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
 	 */
 	if (stmt->relation != NULL)
 	{
-		rel = process_single_relation(stmt, &params);
+		rel = process_single_relation(stmt, lockmode, isTopLevel, &params);
 		if (rel == NULL)
 			return;				/* all done */
 	}
@@ -156,10 +298,29 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 					   "REPACK (ANALYZE)"));
 
 	/*
-	 * By here, we know we are in a multi-table situation.  In order to avoid
-	 * holding locks for too long, we want to process each table in its own
-	 * transaction.  This forces us to disallow running inside a user
-	 * transaction block.
+	 * By here, we know we are in a multi-table situation.
+	 *
+	 * Concurrent processing is currently considered rather special (e.g. in
+	 * terms of resources consumed) so it is not performed in bulk.
+	 */
+	if (params.options & CLUOPT_CONCURRENT)
+	{
+		if (rel != NULL)
+		{
+			Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
+			ereport(ERROR,
+					errmsg("REPACK CONCURRENTLY not supported for partitioned tables"),
+					errhint("Consider running the command for individual partitions."));
+		}
+		else
+			ereport(ERROR,
+					errmsg("REPACK CONCURRENTLY requires explicit table name"));
+	}
+
+	/*
+	 * In order to avoid holding locks for too long, we want to process each
+	 * table in its own transaction.  This forces us to disallow running
+	 * inside a user transaction block.
 	 */
 	PreventInTransactionBlock(isTopLevel, RepackCommandAsString(stmt->command));
 
@@ -168,6 +329,12 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 										   "Repack",
 										   ALLOCSET_DEFAULT_SIZES);
 
+	/*
+	 * Since we open a new transaction for each relation, we have to check
+	 * that the relation still is what we think it is.
+	 *
+	 * In single-transaction CLUSTER, we don't need the overhead.
+	 */
 	params.options |= CLUOPT_RECHECK;
 
 	/*
@@ -253,7 +420,7 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 		 * Open the target table, coping with the case where it has been
 		 * dropped.
 		 */
-		rel = try_table_open(rtc->tableOid, AccessExclusiveLock);
+		rel = try_table_open(rtc->tableOid, lockmode);
 		if (rel == NULL)
 		{
 			CommitTransactionCommand();
@@ -264,7 +431,7 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 		PushActiveSnapshot(GetTransactionSnapshot());
 
 		/* Process this table */
-		cluster_rel(stmt->command, rel, rtc->indexOid, &params);
+		cluster_rel(stmt->command, rel, rtc->indexOid, &params, isTopLevel);
 		/* cluster_rel closes the relation, but keeps lock */
 
 		PopActiveSnapshot();
@@ -278,6 +445,22 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	MemoryContextDelete(repack_context);
 }
 
+/*
+ * In the non-concurrent case, we obtain AccessExclusiveLock throughout the
+ * operation to avoid any lock-upgrade hazards.  In the concurrent case, we
+ * grab ShareUpdateExclusiveLock (jsut like VACUUM) for most of the
+ * processing and only acquire AccessExclusiveLock at the end, to swap the
+ * relation -- supposedly for a short time.
+ */
+static LOCKMODE
+RepackLockLevel(bool concurrent)
+{
+	if (concurrent)
+		return ShareUpdateExclusiveLock;
+	else
+		return AccessExclusiveLock;
+}
+
 /*
  * cluster_rel
  *
@@ -293,22 +476,51 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
  * If indexOid is InvalidOid, the table will be rewritten in physical order
  * instead of index order.
  *
+ * Note that, in the concurrent case, the function releases the lock at some
+ * point, in order to get AccessExclusiveLock for the final steps (i.e. to
+ * swap the relation files). To make things simpler, the caller should expect
+ * OldHeap to be closed on return, regardless CLUOPT_CONCURRENT. (The
+ * AccessExclusiveLock is kept till the end of the transaction.)
+ *
  * 'cmd' indicates which command is being executed, to be used for error
  * messages.
  */
 void
 cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
-			ClusterParams *params)
+			ClusterParams *params, bool isTopLevel)
 {
 	Oid			tableOid = RelationGetRelid(OldHeap);
+	Relation	index;
+	LOCKMODE	lmode;
 	Oid			save_userid;
 	int			save_sec_context;
 	int			save_nestlevel;
 	bool		verbose = ((params->options & CLUOPT_VERBOSE) != 0);
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
-	Relation	index;
+	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
+	Oid			ident_idx = InvalidOid;
 
-	Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
+	/* Determine the lock mode to use. */
+	lmode = RepackLockLevel(concurrent);
+
+	/*
+	 * Check some preconditions in the concurrent case.  This also obtains the
+	 * replica index OID.
+	 */
+	if (concurrent)
+	{
+		/*
+		 * Make sure we're not in a transaction block.
+		 *
+		 * The reason is that repack_setup_logical_decoding() could deadlock
+		 * if there's an XID already assigned.  It would be possible to run in
+		 * a transaction block if we had no XID, but this restriction is
+		 * simpler for users to understand and we don't lose anything.
+		 */
+		PreventInTransactionBlock(isTopLevel, "REPACK (CONCURRENTLY)");
+
+		check_repack_concurrently_requirements(OldHeap, &ident_idx);
+	}
 
 	/* Check for user-requested abort. */
 	CHECK_FOR_INTERRUPTS();
@@ -328,16 +540,15 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	RestrictSearchPath();
 
 	/*
-	 * Since we may open a new transaction for each relation, we have to check
-	 * that the relation still is what we think it is.
+	 * Recheck that the relation is still what it was when we started.
 	 *
-	 * If this is a single-transaction CLUSTER, we can skip these tests. We
-	 * *must* skip the one on indisclustered since it would reject an attempt
-	 * to cluster a not-previously-clustered index.
+	 * Note that it's critical to skip this in single-relation CLUSTER;
+	 * otherwise, we would reject an attempt to cluster using a
+	 * not-previously-clustered index.
 	 */
 	if (recheck &&
 		!cluster_rel_recheck(cmd, OldHeap, indexOid, save_userid,
-							 params->options))
+							 lmode, params->options))
 		goto out;
 
 	/*
@@ -353,6 +564,12 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 				errmsg("cannot execute %s on a shared catalog",
 					   RepackCommandAsString(cmd)));
 
+	/*
+	 * The CONCURRENTLY case should have been rejected earlier because it does
+	 * not support system catalogs.
+	 */
+	Assert(!(OldHeap->rd_rel->relisshared && concurrent));
+
 	/*
 	 * Don't process temp tables of other backends ... their local buffer
 	 * manager is not going to cope.
@@ -374,7 +591,7 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	if (OidIsValid(indexOid))
 	{
 		/* verify the index is good and lock it */
-		check_index_is_clusterable(OldHeap, indexOid, AccessExclusiveLock);
+		check_index_is_clusterable(OldHeap, indexOid, lmode);
 		/* also open it */
 		index = index_open(indexOid, NoLock);
 	}
@@ -409,7 +626,9 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	if (OldHeap->rd_rel->relkind == RELKIND_MATVIEW &&
 		!RelationIsPopulated(OldHeap))
 	{
-		relation_close(OldHeap, AccessExclusiveLock);
+		if (index)
+			index_close(index, lmode);
+		relation_close(OldHeap, lmode);
 		goto out;
 	}
 
@@ -422,11 +641,34 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 * invalid, because we move tuples around.  Promote them to relation
 	 * locks.  Predicate locks on indexes will be promoted when they are
 	 * reindexed.
+	 *
+	 * During concurrent processing, the heap as well as its indexes stay in
+	 * operation, so we postpone this step until they are locked using
+	 * AccessExclusiveLock near the end of the processing.
 	 */
-	TransferPredicateLocksToHeapRelation(OldHeap);
+	if (!concurrent)
+		TransferPredicateLocksToHeapRelation(OldHeap);
 
 	/* rebuild_relation does all the dirty work */
-	rebuild_relation(OldHeap, index, verbose);
+	PG_TRY();
+	{
+		rebuild_relation(OldHeap, index, verbose, ident_idx);
+	}
+	PG_FINALLY();
+	{
+		if (concurrent)
+		{
+			/*
+			 * Since during normal operation the worker was already asked to
+			 * exit, stopping it explicitly is especially important on ERROR.
+			 * However it still seems a good practice to make sure that the
+			 * worker never survives the REPACK command.
+			 */
+			stop_repack_decoding_worker();
+		}
+	}
+	PG_END_TRY();
+
 	/* rebuild_relation closes OldHeap, and index if valid */
 
 out:
@@ -445,14 +687,14 @@ out:
  */
 static bool
 cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid,
-					Oid userid, int options)
+					Oid userid, LOCKMODE lmode, int options)
 {
 	Oid			tableOid = RelationGetRelid(OldHeap);
 
 	/* Check that the user still has privileges for the relation */
 	if (!repack_is_permitted_for_relation(cmd, tableOid, userid))
 	{
-		relation_close(OldHeap, AccessExclusiveLock);
+		relation_close(OldHeap, lmode);
 		return false;
 	}
 
@@ -466,7 +708,7 @@ cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (RELATION_IS_OTHER_TEMP(OldHeap))
 	{
-		relation_close(OldHeap, AccessExclusiveLock);
+		relation_close(OldHeap, lmode);
 		return false;
 	}
 
@@ -477,7 +719,7 @@ cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		 */
 		if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(indexOid)))
 		{
-			relation_close(OldHeap, AccessExclusiveLock);
+			relation_close(OldHeap, lmode);
 			return false;
 		}
 
@@ -488,7 +730,7 @@ cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		if ((options & CLUOPT_RECHECK_ISCLUSTERED) != 0 &&
 			!get_index_isclustered(indexOid))
 		{
-			relation_close(OldHeap, AccessExclusiveLock);
+			relation_close(OldHeap, lmode);
 			return false;
 		}
 	}
@@ -500,7 +742,7 @@ cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid,
  * Verify that the specified heap and index are valid to cluster on
  *
  * Side effect: obtains lock on the index.  The caller may
- * in some cases already have AccessExclusiveLock on the table, but
+ * in some cases already have a lock of the same strength on the table, but
  * not in all cases so we can't rely on the table-level lock for
  * protection here.
  */
@@ -626,17 +868,94 @@ mark_index_clustered(Relation rel, Oid indexOid, bool is_internal)
 }
 
 /*
- * rebuild_relation: rebuild an existing relation in index or physical order
+ * Check if the CONCURRENTLY option is legal for the relation.
  *
- * OldHeap: table to rebuild.
- * index: index to cluster by, or NULL to rewrite in physical order.
- *
- * On entry, heap and index (if one is given) must be open, and
- * AccessExclusiveLock held on them.
- * On exit, they are closed, but locks on them are not released.
+ * *Ident_idx_p receives OID of the identity index.
  */
 static void
-rebuild_relation(Relation OldHeap, Relation index, bool verbose)
+check_repack_concurrently_requirements(Relation rel, Oid *ident_idx_p)
+{
+	char		relpersistence,
+				replident;
+	Oid			ident_idx;
+
+	/* Data changes in system relations are not logically decoded. */
+	if (IsCatalogRelation(rel))
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot repack relation \"%s\"",
+					   RelationGetRelationName(rel)),
+				errhint("REPACK CONCURRENTLY is not supported for catalog relations."));
+
+	/*
+	 * reorderbuffer.c does not seem to handle processing of TOAST relation
+	 * alone.
+	 */
+	if (IsToastRelation(rel))
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot repack relation \"%s\"",
+					   RelationGetRelationName(rel)),
+				errhint("REPACK CONCURRENTLY is not supported for TOAST relations, unless the main relation is repacked too."));
+
+	relpersistence = rel->rd_rel->relpersistence;
+	if (relpersistence != RELPERSISTENCE_PERMANENT)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot repack relation \"%s\"",
+					   RelationGetRelationName(rel)),
+				errhint("REPACK CONCURRENTLY is only allowed for permanent relations."));
+
+	/* With NOTHING, WAL does not contain the old tuple. */
+	replident = rel->rd_rel->relreplident;
+	if (replident == REPLICA_IDENTITY_NOTHING)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot repack relation \"%s\"",
+					   RelationGetRelationName(rel)),
+				errhint("Relation \"%s\" has insufficient replication identity.",
+						RelationGetRelationName(rel)));
+
+	/*
+	 * If the identity index is not set due to replica identity being, PK
+	 * might exist.
+	 */
+	ident_idx = RelationGetReplicaIndex(rel);
+	if (!OidIsValid(ident_idx) && OidIsValid(rel->rd_pkindex))
+		ident_idx = rel->rd_pkindex;
+	if (!OidIsValid(ident_idx))
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("cannot process relation \"%s\"",
+					   RelationGetRelationName(rel)),
+				errhint("Relation \"%s\" has no identity index.",
+						RelationGetRelationName(rel)));
+
+	*ident_idx_p = ident_idx;
+}
+
+
+/*
+ * rebuild_relation: rebuild an existing relation in index or physical order
+ *
+ * OldHeap: table to rebuild.  See cluster_rel() for comments on the required
+ * lock strength.
+ *
+ * index: index to cluster by, or NULL to rewrite in physical order.
+ *
+ * ident_idx: identity index, to handle replaying of concurrent data changes
+ * to the new heap. InvalidOid if there's no CONCURRENTLY option.
+ *
+ * On entry, heap and index (if one is given) must be open, and the
+ * appropriate lock held on them -- AccessExclusiveLock for exclusive
+ * processing and ShareUpdateExclusiveLock for concurrent processing.
+ *
+ * On exit, they are closed, but still locked with AccessExclusiveLock.
+ * (The function handles the lock upgrade if 'concurrent' is true.)
+ */
+static void
+rebuild_relation(Relation OldHeap, Relation index, bool verbose,
+				 Oid ident_idx)
 {
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
@@ -644,13 +963,55 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose)
 	Oid			OIDNewHeap;
 	Relation	NewHeap;
 	char		relpersistence;
-	bool		is_system_catalog;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
+	bool		concurrent = OidIsValid(ident_idx);
+	Snapshot	snapshot = NULL;
+#if USE_ASSERT_CHECKING
+	LOCKMODE	lmode;
 
-	Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false) &&
-		   (index == NULL || CheckRelationLockedByMe(index, AccessExclusiveLock, false)));
+	lmode = concurrent ? ShareUpdateExclusiveLock : AccessExclusiveLock;
+
+	Assert(CheckRelationLockedByMe(OldHeap, lmode, false));
+	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
+#endif
+
+	if (concurrent)
+	{
+		/*
+		 * The worker needs to be member of the locking group we're the leader
+		 * of. We ought to become the leader before the worker starts. The
+		 * worker will join the group as soon as it starts.
+		 *
+		 * This is to make sure that the deadlock described below is
+		 * detectable by deadlock.c: if the worker waits for a transaction to
+		 * complete and we are waiting for the worker output, then effectively
+		 * we (i.e. this backend) are waiting for that transaction.
+		 */
+		BecomeLockGroupLeader();
+
+		/*
+		 * Start the worker that decodes data changes applied while we're
+		 * copying the table contents.
+		 *
+		 * Note that the worker has to wait for all transactions with XID
+		 * already assigned to finish. If some of those transactions is
+		 * waiting for a lock conflicting with ShareUpdateExclusiveLock on our
+		 * table (e.g.  it runs CREATE INDEX), we can end up in a deadlock.
+		 * Not sure this risk is worth unlocking/locking the table (and its
+		 * clustering index) and checking again if it's still eligible for
+		 * REPACK CONCURRENTLY.
+		 */
+		start_repack_decoding_worker(tableOid);
+
+		/*
+		 * Wait until the worker has the initial snapshot and retrieve it.
+		 */
+		snapshot = get_initial_snapshot(decoding_worker);
+
+		PushActiveSnapshot(snapshot);
+	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
 	if (index != NULL)
@@ -658,7 +1019,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose)
 
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
-	is_system_catalog = IsSystemRelation(OldHeap);
 
 	/*
 	 * Create the transient table that will receive the re-ordered data.
@@ -674,30 +1034,59 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose)
 	NewHeap = table_open(OIDNewHeap, NoLock);
 
 	/* Copy the heap data into the new table in the desired order */
-	copy_table_data(NewHeap, OldHeap, index, verbose,
+	copy_table_data(NewHeap, OldHeap, index, snapshot, verbose,
 					&swap_toast_by_content, &frozenXid, &cutoffMulti);
 
+	/* The historic snapshot won't be needed anymore. */
+	if (snapshot)
+	{
+		PopActiveSnapshot();
+		UpdateActiveSnapshotCommandId();
+	}
 
-	/* Close relcache entries, but keep lock until transaction commit */
-	table_close(OldHeap, NoLock);
-	if (index)
-		index_close(index, NoLock);
+	if (concurrent)
+	{
+		Assert(!swap_toast_by_content);
 
-	/*
-	 * Close the new relation so it can be dropped as soon as the storage is
-	 * swapped. The relation is not visible to others, so no need to unlock it
-	 * explicitly.
-	 */
-	table_close(NewHeap, NoLock);
+		/*
+		 * Close the index, but keep the lock. Both heaps will be closed by
+		 * the following call.
+		 */
+		if (index)
+			index_close(index, NoLock);
 
-	/*
-	 * Swap the physical files of the target and transient tables, then
-	 * rebuild the target's indexes and throw away the transient table.
-	 */
-	finish_heap_swap(tableOid, OIDNewHeap, is_system_catalog,
-					 swap_toast_by_content, false, true,
-					 frozenXid, cutoffMulti,
-					 relpersistence);
+		rebuild_relation_finish_concurrent(NewHeap, OldHeap, ident_idx,
+										   frozenXid, cutoffMulti);
+
+		pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
+									 PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
+	}
+	else
+	{
+		bool		is_system_catalog = IsSystemRelation(OldHeap);
+
+		/* Close relcache entries, but keep lock until transaction commit */
+		table_close(OldHeap, NoLock);
+		if (index)
+			index_close(index, NoLock);
+
+		/*
+		 * Close the new relation so it can be dropped as soon as the storage
+		 * is swapped. The relation is not visible to others, so no need to
+		 * unlock it explicitly.
+		 */
+		table_close(NewHeap, NoLock);
+
+		/*
+		 * Swap the physical files of the target and transient tables, then
+		 * rebuild the target's indexes and throw away the transient table.
+		 */
+		finish_heap_swap(tableOid, OIDNewHeap, is_system_catalog,
+						 swap_toast_by_content, false, true,
+						 true,	/* reindex */
+						 frozenXid, cutoffMulti,
+						 relpersistence);
+	}
 }
 
 
@@ -832,15 +1221,18 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 /*
  * Do the physical copying of table data.
  *
+ * 'snapshot' and 'decoding_ctx': see table_relation_copy_for_cluster(). Pass
+ * iff concurrent processing is required.
+ *
  * There are three output parameters:
  * *pSwapToastByContent is set true if toast tables must be swapped by content.
  * *pFreezeXid receives the TransactionId used as freeze cutoff point.
  * *pCutoffMulti receives the MultiXactId used as a cutoff point.
  */
 static void
-copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verbose,
-				bool *pSwapToastByContent, TransactionId *pFreezeXid,
-				MultiXactId *pCutoffMulti)
+copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
+				Snapshot snapshot, bool verbose, bool *pSwapToastByContent,
+				TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
 {
 	Relation	relRelation;
 	HeapTuple	reltup;
@@ -857,6 +1249,10 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb
 	int			elevel = verbose ? INFO : DEBUG2;
 	PGRUsage	ru0;
 	char	   *nspname;
+	bool		concurrent = snapshot != NULL;
+	LOCKMODE	lmode;
+
+	lmode = concurrent ? ShareUpdateExclusiveLock : AccessExclusiveLock;
 
 	pg_rusage_init(&ru0);
 
@@ -885,7 +1281,7 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb
 	 * will be held till end of transaction.
 	 */
 	if (OldHeap->rd_rel->reltoastrelid)
-		LockRelationOid(OldHeap->rd_rel->reltoastrelid, AccessExclusiveLock);
+		LockRelationOid(OldHeap->rd_rel->reltoastrelid, lmode);
 
 	/*
 	 * If both tables have TOAST tables, perform toast swap by content.  It is
@@ -894,7 +1290,8 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb
 	 * swap by links.  This is okay because swap by content is only essential
 	 * for system catalogs, and we don't support schema changes for them.
 	 */
-	if (OldHeap->rd_rel->reltoastrelid && NewHeap->rd_rel->reltoastrelid)
+	if (OldHeap->rd_rel->reltoastrelid && NewHeap->rd_rel->reltoastrelid &&
+		!concurrent)
 	{
 		*pSwapToastByContent = true;
 
@@ -915,6 +1312,10 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb
 		 * follow the toast pointers to the wrong place.  (It would actually
 		 * work for values copied over from the old toast table, but not for
 		 * any values that we toast which were previously not toasted.)
+		 *
+		 * This would not work with CONCURRENTLY because we may need to delete
+		 * TOASTed tuples from the new heap. With this hack, we'd delete them
+		 * from the old heap.
 		 */
 		NewHeap->rd_toastoid = OldHeap->rd_rel->reltoastrelid;
 	}
@@ -990,7 +1391,8 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb
 	 * values (e.g. because the AM doesn't use freezing).
 	 */
 	table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
-									cutoffs.OldestXmin, &cutoffs.FreezeLimit,
+									cutoffs.OldestXmin, snapshot,
+									&cutoffs.FreezeLimit,
 									&cutoffs.MultiXactCutoff,
 									&num_tuples, &tups_vacuumed,
 									&tups_recently_dead);
@@ -999,7 +1401,11 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb
 	*pFreezeXid = cutoffs.FreezeLimit;
 	*pCutoffMulti = cutoffs.MultiXactCutoff;
 
-	/* Reset rd_toastoid just to be tidy --- it shouldn't be looked at again */
+	/*
+	 * Reset rd_toastoid just to be tidy --- it shouldn't be looked at again.
+	 * In the CONCURRENTLY case, we need to set it again before applying the
+	 * concurrent changes.
+	 */
 	NewHeap->rd_toastoid = InvalidOid;
 
 	num_pages = RelationGetNumberOfBlocks(NewHeap);
@@ -1457,14 +1863,13 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 				 bool swap_toast_by_content,
 				 bool check_constraints,
 				 bool is_internal,
+				 bool reindex,
 				 TransactionId frozenXid,
 				 MultiXactId cutoffMulti,
 				 char newrelpersistence)
 {
 	ObjectAddress object;
 	Oid			mapped_tables[4];
-	int			reindex_flags;
-	ReindexParams reindex_params = {0};
 	int			i;
 
 	/* Report that we are now swapping relation files */
@@ -1490,39 +1895,47 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 	if (is_system_catalog)
 		CacheInvalidateCatalog(OIDOldHeap);
 
-	/*
-	 * Rebuild each index on the relation (but not the toast table, which is
-	 * all-new at this point).  It is important to do this before the DROP
-	 * step because if we are processing a system catalog that will be used
-	 * during DROP, we want to have its indexes available.  There is no
-	 * advantage to the other order anyway because this is all transactional,
-	 * so no chance to reclaim disk space before commit.  We do not need a
-	 * final CommandCounterIncrement() because reindex_relation does it.
-	 *
-	 * Note: because index_build is called via reindex_relation, it will never
-	 * set indcheckxmin true for the indexes.  This is OK even though in some
-	 * sense we are building new indexes rather than rebuilding existing ones,
-	 * because the new heap won't contain any HOT chains at all, let alone
-	 * broken ones, so it can't be necessary to set indcheckxmin.
-	 */
-	reindex_flags = REINDEX_REL_SUPPRESS_INDEX_USE;
-	if (check_constraints)
-		reindex_flags |= REINDEX_REL_CHECK_CONSTRAINTS;
+	if (reindex)
+	{
+		int			reindex_flags;
+		ReindexParams reindex_params = {0};
 
-	/*
-	 * Ensure that the indexes have the same persistence as the parent
-	 * relation.
-	 */
-	if (newrelpersistence == RELPERSISTENCE_UNLOGGED)
-		reindex_flags |= REINDEX_REL_FORCE_INDEXES_UNLOGGED;
-	else if (newrelpersistence == RELPERSISTENCE_PERMANENT)
-		reindex_flags |= REINDEX_REL_FORCE_INDEXES_PERMANENT;
+		/*
+		 * Rebuild each index on the relation (but not the toast table, which
+		 * is all-new at this point).  It is important to do this before the
+		 * DROP step because if we are processing a system catalog that will
+		 * be used during DROP, we want to have its indexes available.  There
+		 * is no advantage to the other order anyway because this is all
+		 * transactional, so no chance to reclaim disk space before commit. We
+		 * do not need a final CommandCounterIncrement() because
+		 * reindex_relation does it.
+		 *
+		 * Note: because index_build is called via reindex_relation, it will
+		 * never set indcheckxmin true for the indexes.  This is OK even
+		 * though in some sense we are building new indexes rather than
+		 * rebuilding existing ones, because the new heap won't contain any
+		 * HOT chains at all, let alone broken ones, so it can't be necessary
+		 * to set indcheckxmin.
+		 */
+		reindex_flags = REINDEX_REL_SUPPRESS_INDEX_USE;
+		if (check_constraints)
+			reindex_flags |= REINDEX_REL_CHECK_CONSTRAINTS;
 
-	/* Report that we are now reindexing relations */
-	pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
-								 PROGRESS_REPACK_PHASE_REBUILD_INDEX);
+		/*
+		 * Ensure that the indexes have the same persistence as the parent
+		 * relation.
+		 */
+		if (newrelpersistence == RELPERSISTENCE_UNLOGGED)
+			reindex_flags |= REINDEX_REL_FORCE_INDEXES_UNLOGGED;
+		else if (newrelpersistence == RELPERSISTENCE_PERMANENT)
+			reindex_flags |= REINDEX_REL_FORCE_INDEXES_PERMANENT;
 
-	reindex_relation(NULL, OIDOldHeap, reindex_flags, &reindex_params);
+		/* Report that we are now reindexing relations */
+		pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
+									 PROGRESS_REPACK_PHASE_REBUILD_INDEX);
+
+		reindex_relation(NULL, OIDOldHeap, reindex_flags, &reindex_params);
+	}
 
 	/* Report that we are now doing clean up */
 	pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
@@ -1566,6 +1979,17 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 	object.objectId = OIDNewHeap;
 	object.objectSubId = 0;
 
+	if (!reindex)
+	{
+		/*
+		 * Make sure the changes in pg_class are visible. This is especially
+		 * important if !swap_toast_by_content, so that the correct TOAST
+		 * relation is dropped. (reindex_relation() above did not help in this
+		 * case))
+		 */
+		CommandCounterIncrement();
+	}
+
 	/*
 	 * The new relation is local to our transaction and we know nothing
 	 * depends on it, so DROP_RESTRICT should be OK.
@@ -1605,7 +2029,7 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 
 			/* Get the associated valid index to be renamed */
 			toastidx = toast_get_valid_index(newrel->rd_rel->reltoastrelid,
-											 NoLock);
+											 AccessExclusiveLock);
 
 			/* rename the toast table ... */
 			snprintf(NewToastName, NAMEDATALEN, "pg_toast_%u",
@@ -1876,7 +2300,8 @@ repack_is_permitted_for_relation(RepackCommand cmd, Oid relid, Oid userid)
  * case, if an index name is given, it's up to the caller to resolve it.
  */
 static Relation
-process_single_relation(RepackStmt *stmt, ClusterParams *params)
+process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
+						ClusterParams *params)
 {
 	Relation	rel;
 	Oid			tableOid;
@@ -1893,13 +2318,9 @@ process_single_relation(RepackStmt *stmt, ClusterParams *params)
 				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				errmsg("ANALYZE option must be specified when a column list is provided"));
 
-	/*
-	 * Find, lock, and check permissions on the table.  We obtain
-	 * AccessExclusiveLock right away to avoid lock-upgrade hazard in the
-	 * single-transaction case.
-	 */
+	/* Find, lock, and check permissions on the table. */
 	tableOid = RangeVarGetRelidExtended(stmt->relation->relation,
-										AccessExclusiveLock,
+										lockmode,
 										0,
 										RangeVarCallbackMaintainsTable,
 										NULL);
@@ -1924,13 +2345,14 @@ process_single_relation(RepackStmt *stmt, ClusterParams *params)
 		return rel;
 	else
 	{
-		Oid			indexOid;
+		Oid			indexOid = InvalidOid;
 
 		indexOid = determine_clustered_index(rel, stmt->usingindex,
 											 stmt->indexname);
 		if (OidIsValid(indexOid))
-			check_index_is_clusterable(rel, indexOid, AccessExclusiveLock);
-		cluster_rel(stmt->command, rel, indexOid, params);
+			check_index_is_clusterable(rel, indexOid, lockmode);
+
+		cluster_rel(stmt->command, rel, indexOid, params, isTopLevel);
 
 		/*
 		 * Do an analyze, if requested.  We close the transaction and start a
@@ -2025,3 +2447,1178 @@ RepackCommandAsString(RepackCommand cmd)
 	}
 	return "???";				/* keep compiler quiet */
 }
+
+/*
+ * Apply all the changes stored in 'file'.
+ */
+static void
+apply_concurrent_changes(BufFile *file, ChangeContext *chgcxt)
+{
+	ConcurrentChangeKind kind = '\0';
+	Relation	rel = chgcxt->cc_rel;
+	TupleTableSlot *spilled_tuple;
+	TupleTableSlot *old_update_tuple;
+	TupleTableSlot *ondisk_tuple;
+	MemoryContext apply_cxt;
+	bool		have_old_tuple = false;
+
+	/*
+	 * Use a separate memory context for the tuples and any memory needed to
+	 * process them.
+	 *
+	 * XXX would this be better with GenerationContextCreate?
+	 */
+	apply_cxt = AllocSetContextCreate(TopTransactionContext,
+									  "REPACK - apply",
+									  ALLOCSET_DEFAULT_SIZES);
+
+	spilled_tuple = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+											 &TTSOpsVirtual);
+	ondisk_tuple = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+											table_slot_callbacks(rel));
+	old_update_tuple = MakeSingleTupleTableSlot(RelationGetDescr(rel),
+												&TTSOpsVirtual);
+
+	while (true)
+	{
+		size_t		nread;
+		ConcurrentChangeKind prevkind = kind;
+
+		CHECK_FOR_INTERRUPTS();
+
+		nread = BufFileReadMaybeEOF(file, &kind, 1, true);
+		if (nread == 0)		/* done with the file? */
+			break;
+
+		/*
+		 * If this is the old tuple for an update, read it into the tuple slot
+		 * and go to the next one.  The update itself will be executed on the
+		 * next iteration, when we receive the NEW tuple.
+		 */
+		if (kind == CHANGE_UPDATE_OLD)
+		{
+			restore_tuple(file, rel, old_update_tuple);
+			have_old_tuple = true;
+			continue;
+		}
+
+		/*
+		 * Just before an UPDATE or DELETE, we must update the command
+		 * counter, because the change could refer to a tuple that we have
+		 * just inserted; and before an INSERT, we have to do this also if the
+		 * previous command was either update or delete.
+		 *
+		 * With this approach we don't spend so many CCIs for long strings of
+		 * only INSERTs, which can't affect one another.
+		 */
+		if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE ||
+			(kind == CHANGE_INSERT && (prevkind == CHANGE_UPDATE_NEW ||
+									   prevkind == CHANGE_DELETE)))
+		{
+			CommandCounterIncrement();
+			UpdateActiveSnapshotCommandId();
+		}
+
+		/*
+		 * Now restore the tuple into the slot and execute the change.
+		 */
+		restore_tuple(file, rel, spilled_tuple);
+
+		if (kind == CHANGE_INSERT)
+		{
+			apply_concurrent_insert(rel, spilled_tuple, chgcxt);
+		}
+		else if (kind == CHANGE_DELETE)
+		{
+			bool		found;
+
+			/* Find the tuple to be deleted */
+			found = find_target_tuple(rel, chgcxt, spilled_tuple, ondisk_tuple);
+			if (!found)
+				elog(ERROR, "failed to find target tuple");
+			apply_concurrent_delete(rel, ondisk_tuple);
+		}
+		else if (kind == CHANGE_UPDATE_NEW)
+		{
+			TupleTableSlot *key;
+			bool		found;
+
+			if (have_old_tuple)
+				key = old_update_tuple;
+			else
+				key = spilled_tuple;
+
+			/* Find the tuple to be updated or deleted. */
+			found = find_target_tuple(rel, chgcxt, key, ondisk_tuple);
+			if (!found)
+				elog(ERROR, "failed to find target tuple");
+
+			/*
+			 * If 'tup' contains TOAST pointers, they point to the old
+			 * relation's toast. Copy the corresponding TOAST pointers for the
+			 * new relation from the existing tuple. (The fact that we
+			 * received a TOAST pointer here implies that the attribute hasn't
+			 * changed.)
+			 */
+			adjust_toast_pointers(rel, spilled_tuple, ondisk_tuple);
+
+			apply_concurrent_update(rel, spilled_tuple, ondisk_tuple, chgcxt);
+
+			ExecClearTuple(old_update_tuple);
+			have_old_tuple = false;
+		}
+		else
+			elog(ERROR, "unrecognized kind of change: %d", kind);
+
+		MemoryContextReset(apply_cxt);
+	}
+
+	/* Cleanup. */
+	ExecDropSingleTupleTableSlot(spilled_tuple);
+	ExecDropSingleTupleTableSlot(ondisk_tuple);
+	ExecDropSingleTupleTableSlot(old_update_tuple);
+	MemoryContextDelete(apply_cxt);
+}
+
+/*
+ * Apply an insert from the spill of concurrent changes to the new copy of the
+ * table.
+ */
+static void
+apply_concurrent_insert(Relation rel, TupleTableSlot *slot,
+						ChangeContext *chgcxt)
+{
+	List	   *recheck;
+
+	/* Put the tuple in the table, but make sure it won't be decoded */
+	table_tuple_insert(rel, slot, GetCurrentCommandId(true),
+					   HEAP_INSERT_NO_LOGICAL, NULL);
+
+	/*
+	 * Update indexes with this new tuple.  Because we're merely replaying an
+	 * action that already happened, we have no use for the recheck list of
+	 * indexes returned, so just free it.  XXX or maybe just leave it?
+	 */
+	recheck = ExecInsertIndexTuples(chgcxt->cc_rri,
+									chgcxt->cc_estate,
+									0,
+									slot,
+									NIL, NULL);
+	list_free(recheck);
+
+	pgstat_progress_incr_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, 1);
+
+	ResetPerTupleExprContext(chgcxt->cc_estate);
+}
+
+/*
+ * Apply an update from the spill of concurrent changes to the new copy of the
+ * table.
+ */
+static void
+apply_concurrent_update(Relation rel, TupleTableSlot *spilled_tuple,
+						TupleTableSlot *ondisk_tuple,
+						ChangeContext *chgcxt)
+{
+	LockTupleMode lockmode;
+	TM_FailureData tmfd;
+	TU_UpdateIndexes update_indexes;
+	TM_Result	res;
+	List	   *recheck;
+
+	/*
+	 * Carry out the update, avoiding logical decoding info.
+	 */
+	res = table_tuple_update(rel, &(ondisk_tuple->tts_tid), spilled_tuple,
+							 GetCurrentCommandId(true),
+							 InvalidSnapshot,
+							 InvalidSnapshot,
+							 TABLE_UPDATE_NO_LOGICAL,
+							 &tmfd, &lockmode, &update_indexes);
+	if (res != TM_Ok)
+		ereport(ERROR,
+				errmsg("failed to apply concurrent UPDATE"));
+
+	if (update_indexes != TU_None)
+	{
+		bits32		flags = EIIT_IS_UPDATE;
+
+		if (update_indexes == TU_Summarizing)
+			flags |= EIIT_ONLY_SUMMARIZING;
+		recheck = ExecInsertIndexTuples(chgcxt->cc_rri,
+										chgcxt->cc_estate,
+										flags,
+										spilled_tuple,
+										NIL, NULL);
+		list_free(recheck);
+		ResetPerTupleExprContext(chgcxt->cc_estate);
+	}
+
+	pgstat_progress_incr_param(PROGRESS_REPACK_HEAP_TUPLES_UPDATED, 1);
+}
+
+static void
+apply_concurrent_delete(Relation rel, TupleTableSlot *slot)
+{
+	TM_Result	res;
+	TM_FailureData tmfd;
+
+	/*
+	 * Delete tuple from the new heap.
+	 *
+	 * Do it like in simple_heap_delete(), except for 'wal_logical' (and
+	 * except for 'wait').
+	 */
+	res = table_tuple_delete(rel, &(slot->tts_tid),
+							 GetCurrentCommandId(true),
+							 InvalidSnapshot, InvalidSnapshot,
+							 TABLE_DELETE_NO_LOGICAL,
+							 &tmfd);
+
+	if (res != TM_Ok)
+		ereport(ERROR,
+				errmsg("failed to apply concurrent DELETE"));
+
+	pgstat_progress_incr_param(PROGRESS_REPACK_HEAP_TUPLES_DELETED, 1);
+}
+
+/*
+ * Read tuple from file and put it in the input slot.
+ *
+ * External attributes are stored in separate memory chunks, in order to avoid
+ * exceeding MaxAllocSize - that could happen if the individual attributes are
+ * smaller than MaxAllocSize but the whole tuple is bigger.
+ */
+static void
+restore_tuple(BufFile *file, Relation relation, TupleTableSlot *slot)
+{
+	uint32		t_len;
+	HeapTuple	tup;
+	MemoryContext oldcxt;
+	int			natt_ext;
+
+	oldcxt = MemoryContextSwitchTo(slot->tts_mcxt);
+
+	/* Read the tuple. */
+	BufFileReadExact(file, &t_len, sizeof(t_len));
+	tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len);
+	tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE);
+	BufFileReadExact(file, tup->t_data, t_len);
+	tup->t_len = t_len;
+	ItemPointerSetInvalid(&tup->t_self);
+	tup->t_tableOid = RelationGetRelid(relation);
+
+	/*
+	 * Put the tuple we read in a slot. This deforms it, so that we can hack
+	 * the external attributes in place.
+	 */
+	ExecForceStoreHeapTuple(tup, slot, false);
+
+	/*
+	 * Next, read any attributes we stored separately into the tts_values
+	 * array elements expecting them, if any.  This matches
+	 * repack_store_change.
+	 */
+	BufFileReadExact(file, &natt_ext, sizeof(natt_ext));
+	if (natt_ext > 0)
+	{
+		TupleDesc	desc = slot->tts_tupleDescriptor;
+
+		for (int i = 0; i < desc->natts; i++)
+		{
+			CompactAttribute *attr = TupleDescCompactAttr(desc, i);
+			varlena    *varlen;
+			alignas(uint32) varlena varhdr;
+			void	   *value;
+			Size		varlensz;
+
+			if (attr->attisdropped || attr->attlen != -1)
+				continue;
+			if (slot_attisnull(slot, i + 1))
+				continue;
+			varlen = (varlena *) DatumGetPointer(slot->tts_values[i]);
+			if (!VARATT_IS_EXTERNAL(varlen))
+				continue;
+			slot_getsomeattrs(slot, i + 1);
+
+			BufFileReadExact(file, &varhdr, VARHDRSZ);
+			varlensz = VARSIZE_ANY(&varhdr);
+
+			value = palloc(varlensz);
+			SET_VARSIZE(value, VARSIZE_ANY(&varhdr));
+			BufFileReadExact(file, (char *) value + VARHDRSZ, varlensz - VARHDRSZ);
+
+			slot->tts_values[i] = PointerGetDatum(value);
+			natt_ext--;
+		}
+	}
+
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Adjust 'dest' replacing any EXTERNAL_ONDISK toast pointers with the
+ * corresponding ones from 'src'.
+ */
+static void
+adjust_toast_pointers(Relation relation, TupleTableSlot *dest, TupleTableSlot *src)
+{
+	TupleDesc	desc = dest->tts_tupleDescriptor;
+
+	for (int i = 0; i < desc->natts; i++)
+	{
+		CompactAttribute *attr = TupleDescCompactAttr(desc, i);
+		varlena    *varlena_dst;
+
+		if (attr->attisdropped)
+			continue;
+		if (attr->attlen != -1)
+			continue;
+		if (slot_attisnull(dest, i + 1))
+			continue;
+
+		slot_getsomeattrs(dest, i + 1);
+
+		varlena_dst = (varlena *) DatumGetPointer(dest->tts_values[i]);
+		if (!VARATT_IS_EXTERNAL_ONDISK(varlena_dst))
+			continue;
+		slot_getsomeattrs(src, i + 1);
+
+		/*
+		 * XXX We simply replace the pointer to the Datum from the other one,
+		 * which is probably bogus.
+		 */
+		dest->tts_values[i] = src->tts_values[i];
+	}
+}
+
+/*
+ * Find the tuple to be updated or deleted by the given data change, whose
+ * tuple has already been loaded into locator.
+ *
+ * If the tuple is found, put it in retrieved and return true.  If the tuple is
+ * not found, return false.
+ */
+static bool
+find_target_tuple(Relation rel, ChangeContext *chgcxt, TupleTableSlot *locator,
+				  TupleTableSlot *retrieved)
+{
+	Form_pg_index idx = chgcxt->cc_ident_index->rd_index;
+	IndexScanDesc scan;
+	bool		retval;
+
+	/*
+	 * Scan key is passed by caller, so it does not have to be constructed
+	 * multiple times. Key entries have all fields initialized, except for
+	 * sk_argument.
+	 *
+	 * Use the incoming tuple to finalize the scan key.
+	 */
+	for (int i = 0; i < chgcxt->cc_ident_key_nentries; i++)
+	{
+		ScanKey		entry = &chgcxt->cc_ident_key[i];
+		AttrNumber	attno = idx->indkey.values[i];
+
+		entry->sk_argument = locator->tts_values[attno - 1];
+		Assert(!locator->tts_isnull[attno - 1]);
+	}
+
+	/* XXX no instrumentation for now */
+	scan = index_beginscan(rel, chgcxt->cc_ident_index, GetActiveSnapshot(),
+						   NULL, chgcxt->cc_ident_key_nentries, 0);
+	index_rescan(scan, chgcxt->cc_ident_key, chgcxt->cc_ident_key_nentries, NULL, 0);
+	retval = index_getnext_slot(scan, ForwardScanDirection, retrieved);
+	index_endscan(scan);
+
+	return retval;
+}
+
+/*
+ * Decode and apply concurrent changes, up to (and including) the record whose
+ * LSN is 'end_of_wal'.
+ *
+ * XXX the names "process_concurrent_changes" and "apply_concurrent_changes"
+ * are far too similar to each other.
+ */
+static void
+process_concurrent_changes(XLogRecPtr end_of_wal, ChangeContext *chgcxt, bool done)
+{
+	DecodingWorkerShared *shared;
+	char		fname[MAXPGPATH];
+	BufFile    *file;
+
+	pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
+								 PROGRESS_REPACK_PHASE_CATCH_UP);
+
+	/* Ask the worker for the file. */
+	shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg);
+	SpinLockAcquire(&shared->mutex);
+	shared->lsn_upto = end_of_wal;
+	shared->done = done;
+	SpinLockRelease(&shared->mutex);
+
+	/*
+	 * The worker needs to finish processing of the current WAL record. Even
+	 * if it's idle, it'll need to close the output file. Thus we're likely to
+	 * wait, so prepare for sleep.
+	 */
+	ConditionVariablePrepareToSleep(&shared->cv);
+	for (;;)
+	{
+		int			last_exported;
+
+		SpinLockAcquire(&shared->mutex);
+		last_exported = shared->last_exported;
+		SpinLockRelease(&shared->mutex);
+
+		/*
+		 * Has the worker exported the file we are waiting for?
+		 */
+		if (last_exported == chgcxt->cc_file_seq)
+			break;
+
+		ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+	}
+	ConditionVariableCancelSleep();
+
+	/* Open the file. */
+	DecodingWorkerFileName(fname, shared->relid, chgcxt->cc_file_seq);
+	file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+	apply_concurrent_changes(file, chgcxt);
+
+	BufFileClose(file);
+
+	/* Get ready for the next file. */
+	chgcxt->cc_file_seq++;
+}
+
+/*
+ * Initialize the ChangeContext struct for the given relation, with
+ * the given index as identity index.
+ */
+static void
+initialize_change_context(ChangeContext *chgcxt,
+						  Relation relation, Oid ident_index_id)
+{
+	chgcxt->cc_rel = relation;
+
+	/* Only initialize fields needed by ExecInsertIndexTuples(). */
+	chgcxt->cc_estate = CreateExecutorState();
+
+	chgcxt->cc_rri = (ResultRelInfo *) palloc(sizeof(ResultRelInfo));
+	InitResultRelInfo(chgcxt->cc_rri, relation, 0, 0, 0);
+	ExecOpenIndices(chgcxt->cc_rri, false);
+
+	/*
+	 * The table's relcache entry already has the relcache entry for the
+	 * identity index; find that.
+	 */
+	chgcxt->cc_ident_index = NULL;
+	for (int i = 0; i < chgcxt->cc_rri->ri_NumIndices; i++)
+	{
+		Relation	ind_rel;
+
+		ind_rel = chgcxt->cc_rri->ri_IndexRelationDescs[i];
+		if (ind_rel->rd_id == ident_index_id)
+		{
+			chgcxt->cc_ident_index = ind_rel;
+			break;
+		}
+	}
+	if (chgcxt->cc_ident_index == NULL)
+		elog(ERROR, "failed to find identity index");
+
+	/* Set up for scanning said identity index */
+	{
+		Form_pg_index indexForm;
+
+		indexForm = chgcxt->cc_ident_index->rd_index;
+		chgcxt->cc_ident_key_nentries = indexForm->indnkeyatts;
+		chgcxt->cc_ident_key = (ScanKey) palloc_array(ScanKeyData, indexForm->indnkeyatts);
+		for (int i = 0; i < indexForm->indnkeyatts; i++)
+		{
+			ScanKey		entry;
+			Oid			opfamily,
+						opcintype,
+						opno,
+						opcode;
+
+			entry = &chgcxt->cc_ident_key[i];
+
+			opfamily = chgcxt->cc_ident_index->rd_opfamily[i];
+			opcintype = chgcxt->cc_ident_index->rd_opcintype[i];
+			opno = get_opfamily_member(opfamily, opcintype, opcintype,
+									   BTEqualStrategyNumber);
+			if (!OidIsValid(opno))
+				elog(ERROR, "failed to find = operator for type %u", opcintype);
+			opcode = get_opcode(opno);
+			if (!OidIsValid(opcode))
+				elog(ERROR, "failed to find = operator for operator %u", opno);
+
+			/* Initialize everything but argument. */
+			ScanKeyInit(entry,
+						i + 1,
+						BTEqualStrategyNumber, opcode,
+						(Datum) NULL);
+			entry->sk_collation = chgcxt->cc_ident_index->rd_indcollation[i];
+		}
+	}
+
+	chgcxt->cc_file_seq = WORKER_FILE_SNAPSHOT + 1;
+}
+
+/*
+ * Free up resources taken by a ChangeContext.
+ */
+static void
+release_change_context(ChangeContext *chgcxt)
+{
+	ExecCloseIndices(chgcxt->cc_rri);
+	FreeExecutorState(chgcxt->cc_estate);
+	/* XXX are these pfrees necessary? */
+	pfree(chgcxt->cc_rri);
+	pfree(chgcxt->cc_ident_key);
+}
+
+/*
+ * The final steps of rebuild_relation() for concurrent processing.
+ *
+ * On entry, NewHeap is locked in AccessExclusiveLock mode. OldHeap and its
+ * clustering index (if one is passed) are still locked in a mode that allows
+ * concurrent data changes. On exit, both tables and their indexes are closed,
+ * but locked in AccessExclusiveLock mode.
+ */
+static void
+rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
+								   Oid identIdx, TransactionId frozenXid,
+								   MultiXactId cutoffMulti)
+{
+	LOCKMODE	lockmode_old PG_USED_FOR_ASSERTS_ONLY;
+	List	   *ind_oids_new;
+	Oid			old_table_oid = RelationGetRelid(OldHeap);
+	Oid			new_table_oid = RelationGetRelid(NewHeap);
+	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	ListCell   *lc,
+			   *lc2;
+	char		relpersistence;
+	bool		is_system_catalog;
+	Oid			ident_idx_new;
+	XLogRecPtr	wal_insert_ptr,
+				end_of_wal;
+	char		dummy_rec_data = '\0';
+	Relation   *ind_refs,
+			   *ind_refs_p;
+	int			nind;
+	ChangeContext chgcxt;
+
+	/* Like in cluster_rel(). */
+	lockmode_old = ShareUpdateExclusiveLock;
+	Assert(CheckRelationLockedByMe(OldHeap, lockmode_old, false));
+	/* This is expected from the caller. */
+	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
+
+	/*
+	 * Unlike the exclusive case, we build new indexes for the new relation
+	 * rather than swapping the storage and reindexing the old relation. The
+	 * point is that the index build can take some time, so we do it before we
+	 * get AccessExclusiveLock on the old heap and therefore we cannot swap
+	 * the heap storage yet.
+	 *
+	 * index_create() will lock the new indexes using AccessExclusiveLock - no
+	 * need to change that. At the same time, we use ShareUpdateExclusiveLock
+	 * to lock the existing indexes - that should be enough to prevent others
+	 * from changing them while we're repacking the relation. The lock on
+	 * table should prevent others from changing the index column list, but
+	 * might not be enough for commands like ALTER INDEX ... SET ... (Those
+	 * are not necessarily dangerous, but can make user confused if the
+	 * changes they do get lost due to REPACK.)
+	 */
+	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+
+	/* Find "identity index" on the new relation. */
+	ident_idx_new = InvalidOid;
+	forboth(lc, ind_oids_old, lc2, ind_oids_new)
+	{
+		Oid			ind_old = lfirst_oid(lc);
+		Oid			ind_new = lfirst_oid(lc2);
+
+		if (identIdx == ind_old)
+		{
+			ident_idx_new = ind_new;
+			break;
+		}
+	}
+
+	/* Should not happen, given our lock on the old relation. */
+	if (!OidIsValid(ident_idx_new))
+		ereport(ERROR,
+				errmsg("identity index missing on the new relation"));
+
+	/* Gather information to apply concurrent changes. */
+	initialize_change_context(&chgcxt, NewHeap, ident_idx_new);
+
+	/*
+	 * During testing, wait for another backend to perform concurrent data
+	 * changes which we will process below.
+	 */
+	INJECTION_POINT("repack-concurrently-before-lock", NULL);
+
+	/*
+	 * Flush all WAL records inserted so far (possibly except for the last
+	 * incomplete page; see GetInsertRecPtr), to minimize the amount of data
+	 * we need to flush while holding exclusive lock on the source table.
+	 */
+	wal_insert_ptr = GetInsertRecPtr();
+	XLogFlush(wal_insert_ptr);
+	end_of_wal = GetFlushRecPtr(NULL);
+
+	/*
+	 * Apply concurrent changes first time, to minimize the time we need to
+	 * hold AccessExclusiveLock. (Quite some amount of WAL could have been
+	 * written during the data copying and index creation.)
+	 */
+	process_concurrent_changes(end_of_wal, &chgcxt, false);
+
+	/*
+	 * Acquire AccessExclusiveLock on the table, its TOAST relation (if there
+	 * is one), all its indexes, so that we can swap the files.
+	 */
+	LockRelationOid(old_table_oid, AccessExclusiveLock);
+
+	/*
+	 * Lock all indexes now, not only the clustering one: all indexes need to
+	 * have their files swapped. While doing that, store their relation
+	 * references in an array, to handle predicate locks below.
+	 */
+	ind_refs_p = ind_refs = palloc_array(Relation, list_length(ind_oids_old));
+	nind = 0;
+	foreach_oid(ind_oid, ind_oids_old)
+	{
+		Relation	index;
+
+		index = index_open(ind_oid, AccessExclusiveLock);
+
+		/*
+		 * TODO 1) Do we need to check if ALTER INDEX was executed since the
+		 * new index was created in build_new_indexes()? 2) Specifically for
+		 * the clustering index, should check_index_is_clusterable() be called
+		 * here? (Not sure about the latter: ShareUpdateExclusiveLock on the
+		 * table probably blocks all commands that affect the result of
+		 * check_index_is_clusterable().)
+		 */
+		*ind_refs_p = index;
+		ind_refs_p++;
+		nind++;
+	}
+
+	/*
+	 * Lock the OldHeap's TOAST relation exclusively - again, the lock is
+	 * needed to swap the files.
+	 */
+	if (OidIsValid(OldHeap->rd_rel->reltoastrelid))
+		LockRelationOid(OldHeap->rd_rel->reltoastrelid, AccessExclusiveLock);
+
+	/*
+	 * Tuples and pages of the old heap will be gone, but the heap will stay.
+	 */
+	TransferPredicateLocksToHeapRelation(OldHeap);
+	/* The same for indexes. */
+	for (int i = 0; i < nind; i++)
+	{
+		Relation	index = ind_refs[i];
+
+		TransferPredicateLocksToHeapRelation(index);
+
+		/*
+		 * References to indexes on the old relation are not needed anymore,
+		 * however locks stay till the end of the transaction.
+		 */
+		index_close(index, NoLock);
+	}
+	pfree(ind_refs);
+
+	/*
+	 * Flush anything we see in WAL, to make sure that all changes committed
+	 * while we were waiting for the exclusive lock are available for
+	 * decoding. This should not be necessary if all backends had
+	 * synchronous_commit set, but we can't rely on this setting.
+	 *
+	 * Unfortunately, GetInsertRecPtr() may lag behind the actual insert
+	 * position, and GetLastImportantRecPtr() points at the start of the last
+	 * record rather than at the end. Thus the simplest way to determine the
+	 * insert position is to insert a dummy record and use its LSN.
+	 *
+	 * XXX Consider using GetLastImportantRecPtr() and adding the size of the
+	 * last record (plus the total size of all the page headers the record
+	 * spans)?
+	 */
+	XLogBeginInsert();
+	XLogRegisterData(&dummy_rec_data, 1);
+	wal_insert_ptr = XLogInsert(RM_XLOG_ID, XLOG_NOOP);
+	XLogFlush(wal_insert_ptr);
+	end_of_wal = GetFlushRecPtr(NULL);
+
+	/*
+	 * Apply the concurrent changes again. Indicate that the decoding worker
+	 * won't be needed anymore.
+	 */
+	process_concurrent_changes(end_of_wal, &chgcxt, true);
+
+	/* Remember info about rel before closing OldHeap */
+	relpersistence = OldHeap->rd_rel->relpersistence;
+	is_system_catalog = IsSystemRelation(OldHeap);
+
+	pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
+								 PROGRESS_REPACK_PHASE_SWAP_REL_FILES);
+
+	/*
+	 * Even ShareUpdateExclusiveLock should have prevented others from
+	 * creating / dropping indexes (even using the CONCURRENTLY option), so we
+	 * do not need to check whether the lists match.
+	 */
+	forboth(lc, ind_oids_old, lc2, ind_oids_new)
+	{
+		Oid			ind_old = lfirst_oid(lc);
+		Oid			ind_new = lfirst_oid(lc2);
+		Oid			mapped_tables[4];
+
+		/* Zero out possible results from swapped_relation_files */
+		memset(mapped_tables, 0, sizeof(mapped_tables));
+
+		swap_relation_files(ind_old, ind_new,
+							(old_table_oid == RelationRelationId),
+							false,	/* swap_toast_by_content */
+							true,
+							InvalidTransactionId,
+							InvalidMultiXactId,
+							mapped_tables);
+
+#ifdef USE_ASSERT_CHECKING
+
+		/*
+		 * Concurrent processing is not supported for system relations, so
+		 * there should be no mapped tables.
+		 */
+		for (int i = 0; i < 4; i++)
+			Assert(mapped_tables[i] == 0);
+#endif
+	}
+
+	/* The new indexes must be visible for deletion. */
+	CommandCounterIncrement();
+
+	/* Close the old heap but keep lock until transaction commit. */
+	table_close(OldHeap, NoLock);
+	/* Close the new heap. (We didn't have to open its indexes). */
+	table_close(NewHeap, NoLock);
+
+	/* Cleanup what we don't need anymore. (And close the identity index.) */
+	release_change_context(&chgcxt);
+
+	/*
+	 * Swap the relations and their TOAST relations and TOAST indexes. This
+	 * also drops the new relation and its indexes.
+	 *
+	 * (System catalogs are currently not supported.)
+	 */
+	Assert(!is_system_catalog);
+	finish_heap_swap(old_table_oid, new_table_oid,
+					 is_system_catalog,
+					 false,		/* swap_toast_by_content */
+					 false,
+					 true,
+					 false,		/* reindex */
+					 frozenXid, cutoffMulti,
+					 relpersistence);
+}
+
+/*
+ * Build indexes on NewHeap according to those on OldHeap.
+ *
+ * OldIndexes is the list of index OIDs on OldHeap. The contained indexes end
+ * up locked using ShareUpdateExclusiveLock.
+ *
+ * A list of OIDs of the corresponding indexes created on NewHeap is
+ * returned. The order of items does match, so we can use these arrays to swap
+ * index storage.
+ */
+static List *
+build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+{
+	List	   *result = NIL;
+
+	pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
+								 PROGRESS_REPACK_PHASE_REBUILD_INDEX);
+
+	foreach_oid(ind_oid, OldIndexes)
+	{
+		Oid			ind_oid_new;
+		char	   *newName;
+		Relation	ind;
+
+		ind = index_open(ind_oid, ShareUpdateExclusiveLock);
+
+		newName = ChooseRelationName(get_rel_name(ind_oid),
+									 NULL,
+									 "repacknew",
+									 get_rel_namespace(ind->rd_index->indrelid),
+									 false);
+		ind_oid_new = index_create_copy(NewHeap, false, ind_oid,
+										ind->rd_rel->reltablespace, newName);
+		result = lappend_oid(result, ind_oid_new);
+
+		index_close(ind, NoLock);
+	}
+
+	return result;
+}
+
+/*
+ * Try to start a background worker to perform logical decoding of data
+ * changes applied to relation while REPACK CONCURRENTLY is copying its
+ * contents to a new table.
+ */
+static void
+start_repack_decoding_worker(Oid relid)
+{
+	Size		size;
+	dsm_segment *seg;
+	DecodingWorkerShared *shared;
+	shm_mq	   *mq;
+	shm_mq_handle *mqh;
+	BackgroundWorker bgw;
+
+	/* Setup shared memory. */
+	size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) +
+		BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE);
+	seg = dsm_create(size, 0);
+	shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+	shared->lsn_upto = InvalidXLogRecPtr;
+	shared->done = false;
+	SharedFileSetInit(&shared->sfs, seg);
+	shared->last_exported = -1;
+	SpinLockInit(&shared->mutex);
+	shared->dbid = MyDatabaseId;
+
+	/*
+	 * This is the UserId set in cluster_rel(). Security context shouldn't be
+	 * needed for decoding worker.
+	 */
+	shared->roleid = GetUserId();
+	shared->relid = relid;
+	ConditionVariableInit(&shared->cv);
+	shared->backend_proc = MyProc;
+	shared->backend_pid = MyProcPid;
+	shared->backend_proc_number = MyProcNumber;
+
+	mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue),
+					   REPACK_ERROR_QUEUE_SIZE);
+	shm_mq_set_receiver(mq, MyProc);
+	mqh = shm_mq_attach(mq, seg, NULL);
+
+	memset(&bgw, 0, sizeof(bgw));
+	snprintf(bgw.bgw_name, BGW_MAXLEN,
+			 "REPACK decoding worker for relation \"%s\"",
+			 get_rel_name(relid));
+	snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker");
+	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS |
+		BGWORKER_BACKEND_DATABASE_CONNECTION;
+	bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+	bgw.bgw_restart_time = BGW_NEVER_RESTART;
+	snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres");
+	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain");
+	bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg));
+	bgw.bgw_notify_pid = MyProcPid;
+
+	decoding_worker = palloc0_object(DecodingWorker);
+	if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle))
+		ereport(ERROR,
+				errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED),
+				errmsg("out of background worker slots"),
+				errhint("You might need to increase \"%s\".", "max_worker_processes"));
+
+	decoding_worker->seg = seg;
+	decoding_worker->error_mqh = mqh;
+
+	/*
+	 * The decoding setup must be done before the caller can have XID assigned
+	 * for any reason, otherwise the worker might end up in a deadlock,
+	 * waiting for the caller's transaction to end. Therefore wait here until
+	 * the worker indicates that it has the logical decoding initialized.
+	 */
+	ConditionVariablePrepareToSleep(&shared->cv);
+	for (;;)
+	{
+		bool		initialized;
+
+		SpinLockAcquire(&shared->mutex);
+		initialized = shared->initialized;
+		SpinLockRelease(&shared->mutex);
+
+		if (initialized)
+			break;
+
+		ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+	}
+	ConditionVariableCancelSleep();
+}
+
+/*
+ * Stop the decoding worker and cleanup the related resources.
+ *
+ * The worker stops on its own when it knows there is no more work to do, but
+ * we need to stop it explicitly at least on ERROR in the launching backend.
+ */
+static void
+stop_repack_decoding_worker(void)
+{
+	BgwHandleStatus status;
+
+	/* Haven't reached the worker startup? */
+	if (decoding_worker == NULL)
+		return;
+
+	/* Could not register the worker? */
+	if (decoding_worker->handle == NULL)
+		return;
+
+	TerminateBackgroundWorker(decoding_worker->handle);
+	/* The worker should really exit before the REPACK command does. */
+	HOLD_INTERRUPTS();
+	status = WaitForBackgroundWorkerShutdown(decoding_worker->handle);
+	RESUME_INTERRUPTS();
+
+	if (status == BGWH_POSTMASTER_DIED)
+		ereport(FATAL,
+				errcode(ERRCODE_ADMIN_SHUTDOWN),
+				errmsg("postmaster exited during REPACK command"));
+
+	shm_mq_detach(decoding_worker->error_mqh);
+
+	/*
+	 * If we could not cancel the current sleep due to ERROR, do that before
+	 * we detach from the shared memory the condition variable is located in.
+	 * If we did not, the bgworker ERROR handling code would try and fail
+	 * badly.
+	 */
+	ConditionVariableCancelSleep();
+
+	dsm_detach(decoding_worker->seg);
+	pfree(decoding_worker);
+	decoding_worker = NULL;
+}
+
+/*
+ * Get the initial snapshot from the decoding worker.
+ */
+static Snapshot
+get_initial_snapshot(DecodingWorker *worker)
+{
+	DecodingWorkerShared *shared;
+	char		fname[MAXPGPATH];
+	BufFile    *file;
+	Size		snap_size;
+	char	   *snap_space;
+	Snapshot	snapshot;
+
+	shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg);
+
+	/*
+	 * The worker needs to initialize the logical decoding, which usually
+	 * takes some time. Therefore it makes sense to prepare for the sleep
+	 * first.
+	 */
+	ConditionVariablePrepareToSleep(&shared->cv);
+	for (;;)
+	{
+		int			last_exported;
+
+		SpinLockAcquire(&shared->mutex);
+		last_exported = shared->last_exported;
+		SpinLockRelease(&shared->mutex);
+
+		/*
+		 * Has the worker exported the file we are waiting for?
+		 */
+		if (last_exported == WORKER_FILE_SNAPSHOT)
+			break;
+
+		ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT);
+	}
+	ConditionVariableCancelSleep();
+
+	/* Read the snapshot from a file. */
+	DecodingWorkerFileName(fname, shared->relid, WORKER_FILE_SNAPSHOT);
+	file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false);
+	BufFileReadExact(file, &snap_size, sizeof(snap_size));
+	snap_space = (char *) palloc(snap_size);
+	BufFileReadExact(file, snap_space, snap_size);
+	BufFileClose(file);
+
+	/* Restore it. */
+	snapshot = RestoreSnapshot(snap_space);
+	pfree(snap_space);
+
+	return snapshot;
+}
+
+/*
+ * Generate worker's file name into 'fname', which must be of size MAXPGPATH.
+ * If relations of the same 'relid' happen to be processed at the same time,
+ * they must be from different databases and therefore different backends must
+ * be involved. 
+ */
+void
+DecodingWorkerFileName(char *fname, Oid relid, uint32 seq)
+{
+	/* The PID is already present in the fileset name, so we needn't add it */
+	snprintf(fname, MAXPGPATH, "%u-%u", relid, seq);
+}
+
+/*
+ * Handle receipt of an interrupt indicating a repack worker message.
+ *
+ * Note: this is called within a signal handler!  All we can do is set
+ * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke
+ * ProcessRepackMessages().
+ */
+void
+HandleRepackMessageInterrupt(void)
+{
+	InterruptPending = true;
+	RepackMessagePending = true;
+	SetLatch(MyLatch);
+}
+
+/*
+ * Process any queued protocol messages received from the repack worker.
+ */
+void
+ProcessRepackMessages(void)
+{
+	MemoryContext oldcontext;
+	static MemoryContext hpm_context = NULL;
+
+	/*
+	 * Nothing to do if we haven't launched the worker yet or have already
+	 * terminated it.
+	 */
+	if (decoding_worker == NULL)
+		return;
+
+	/*
+	 * This is invoked from ProcessInterrupts(), and since some of the
+	 * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential
+	 * for recursive calls if more signals are received while this runs.  It's
+	 * unclear that recursive entry would be safe, and it doesn't seem useful
+	 * even if it is safe, so let's block interrupts until done.
+	 */
+	HOLD_INTERRUPTS();
+
+	/*
+	 * Moreover, CurrentMemoryContext might be pointing almost anywhere.  We
+	 * don't want to risk leaking data into long-lived contexts, so let's do
+	 * our work here in a private context that we can reset on each use.
+	 */
+	if (hpm_context == NULL)	/* first time through? */
+		hpm_context = AllocSetContextCreate(TopMemoryContext,
+											"ProcessRepackMessages",
+											ALLOCSET_DEFAULT_SIZES);
+	else
+		MemoryContextReset(hpm_context);
+
+	oldcontext = MemoryContextSwitchTo(hpm_context);
+
+	/* OK to process messages.  Reset the flag saying there are more to do. */
+	RepackMessagePending = false;
+
+	/*
+	 * Read as many messages as we can from the worker, but stop when no more
+	 * messages can be read from the worker without blocking.
+	 */
+	while (true)
+	{
+		shm_mq_result res;
+		Size		nbytes;
+		void	   *data;
+
+		res = shm_mq_receive(decoding_worker->error_mqh, &nbytes,
+							 &data, true);
+		if (res == SHM_MQ_WOULD_BLOCK)
+			break;
+		else if (res == SHM_MQ_SUCCESS)
+		{
+			StringInfoData msg;
+
+			initStringInfo(&msg);
+			appendBinaryStringInfo(&msg, data, nbytes);
+			ProcessRepackMessage(&msg);
+			pfree(msg.data);
+		}
+		else
+		{
+			/*
+			 * The decoding worker is special in that it exits as soon as it
+			 * has its work done. Thus the DETACHED result code is fine.
+			 */
+			Assert(res == SHM_MQ_DETACHED);
+
+			break;
+		}
+	}
+
+	MemoryContextSwitchTo(oldcontext);
+
+	/* Might as well clear the context on our way out */
+	MemoryContextReset(hpm_context);
+
+	RESUME_INTERRUPTS();
+}
+
+/*
+ * Process a single protocol message received from a single parallel worker.
+ */
+static void
+ProcessRepackMessage(StringInfo msg)
+{
+	char		msgtype;
+
+	msgtype = pq_getmsgbyte(msg);
+
+	switch (msgtype)
+	{
+		case PqMsg_ErrorResponse:
+		case PqMsg_NoticeResponse:
+			{
+				ErrorData	edata;
+
+				/* Parse ErrorResponse or NoticeResponse. */
+				pq_parse_errornotice(msg, &edata);
+
+				/* Death of a worker isn't enough justification for suicide. */
+				edata.elevel = Min(edata.elevel, ERROR);
+
+				/*
+				 * Add a context line to show that this is a message propagated
+				 * from the worker.  Otherwise, it can sometimes be confusing
+				 * to understand what actually happened.
+				 */
+				if (edata.context)
+					edata.context = psprintf("%s\n%s", edata.context,
+											 _("REPACK decoding worker"));
+				else
+					edata.context = pstrdup(_("REPACK decoding worker"));
+
+				/* Rethrow error or print notice. */
+				ThrowErrorData(&edata);
+
+				break;
+			}
+
+		default:
+			{
+				elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)",
+					 msgtype, msg->len);
+			}
+	}
+}
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index 81a55a33ef2..539969d6eef 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -893,6 +893,7 @@ static void
 refresh_by_heap_swap(Oid matviewOid, Oid OIDNewHeap, char relpersistence)
 {
 	finish_heap_swap(matviewOid, OIDNewHeap, false, false, true, true,
+					 true,		/* reindex */
 					 RecentXmin, ReadNextMultiXactId(), relpersistence);
 }
 
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index 90c7e37a429..4a0e6f0002b 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -39,6 +39,7 @@ backend_sources += files(
   'proclang.c',
   'propgraphcmds.c',
   'publicationcmds.c',
+  'repack_worker.c',
   'schemacmds.c',
   'seclabel.c',
   'sequence.c',
diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c
new file mode 100644
index 00000000000..35ab1a25c42
--- /dev/null
+++ b/src/backend/commands/repack_worker.c
@@ -0,0 +1,546 @@
+/*-------------------------------------------------------------------------
+ *
+ * repack_worker.c
+ *    Implementation of the background worker for ad-hoc logical decoding
+ *    during REPACK (CONCURRENTLY).
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994-5, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/repack_worker.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/table.h"
+#include "access/xlog_internal.h"
+#include "access/xlogutils.h"
+#include "access/xlogwait.h"
+#include "commands/cluster.h"
+#include "commands/repack_internal.h"
+#include "libpq/pqmq.h"
+#include "replication/snapbuild.h"
+#include "storage/ipc.h"
+#include "storage/proc.h"
+#include "tcop/tcopprot.h"
+#include "utils/memutils.h"
+
+#define REPL_PLUGIN_NAME   "pgoutput_repack"
+
+static void repack_worker_internal(dsm_segment *seg);
+static void RepackWorkerShutdown(int code, Datum arg);
+static LogicalDecodingContext *repack_setup_logical_decoding(Oid relid);
+static void repack_cleanup_logical_decoding(LogicalDecodingContext *ctx);
+static void export_initial_snapshot(Snapshot snapshot,
+									DecodingWorkerShared *shared);
+static bool decode_concurrent_changes(LogicalDecodingContext *ctx,
+									  DecodingWorkerShared *shared);
+
+
+/* Is this process a REPACK worker? */
+static bool is_repack_worker = false;
+
+/* The WAL segment being decoded. */
+static XLogSegNo repack_current_segment = 0;
+
+/*
+ * Keep track of the table we're processing, to skip logical decoding of data
+ * from other relations.
+ */
+static RelFileLocator repacked_rel_locator = {.relNumber = InvalidOid};
+static RelFileLocator repacked_rel_toast_locator = {.relNumber = InvalidOid};
+
+
+/* REPACK decoding worker entry point */
+void
+RepackWorkerMain(Datum main_arg)
+{
+	dsm_segment *seg;
+	DecodingWorkerShared *shared;
+	shm_mq	   *mq;
+	shm_mq_handle *mqh;
+
+	is_repack_worker = true;
+
+	/*
+	 * Override the default bgworker_die() with die() so we can use
+	 * CHECK_FOR_INTERRUPTS().
+	 */
+	pqsignal(SIGTERM, die);
+	BackgroundWorkerUnblockSignals();
+
+	seg = dsm_attach(DatumGetUInt32(main_arg));
+	if (seg == NULL)
+		ereport(ERROR,
+				errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				errmsg("could not map dynamic shared memory segment"));
+
+	shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+	/* Arrange to signal the leader if we exit. */
+	backend_pid = shared->backend_pid;
+	backend_proc_number = shared->backend_proc_number;
+	before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(seg));
+
+	/*
+	 * Join locking group - see the comments around the call of
+	 * start_repack_decoding_worker().
+	 */
+	if (!BecomeLockGroupMember(shared->backend_proc, backend_pid))
+		/* The leader is not running anymore. */
+		return;
+
+	/*
+	 * Setup a queue to send error messages to the backend that launched this
+	 * worker.
+	 */
+	mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue);
+	shm_mq_set_sender(mq, MyProc);
+	mqh = shm_mq_attach(mq, seg, NULL);
+	pq_redirect_to_shm_mq(seg, mqh);
+	pq_set_parallel_leader(shared->backend_pid,
+						   shared->backend_proc_number);
+
+	/* Connect to the database. */
+	BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, 0);
+
+	repack_worker_internal(seg);
+}
+
+static void
+repack_worker_internal(dsm_segment *seg)
+{
+	DecodingWorkerShared *shared;
+	LogicalDecodingContext *decoding_ctx;
+	SharedFileSet *sfs;
+	Snapshot	snapshot;
+
+	/*
+	 * Transaction is needed to open relation, and it also provides us with a
+	 * resource owner.
+	 */
+	StartTransactionCommand();
+
+	shared = (DecodingWorkerShared *) dsm_segment_address(seg);
+
+	/*
+	 * Not sure the spinlock is needed here - the backend should not change
+	 * anything in the shared memory until we have serialized the snapshot.
+	 */
+	SpinLockAcquire(&shared->mutex);
+	Assert(!XLogRecPtrIsValid(shared->lsn_upto));
+	sfs = &shared->sfs;
+	SpinLockRelease(&shared->mutex);
+
+	SharedFileSetAttach(sfs, seg);
+
+	/*
+	 * Prepare to capture the concurrent data changes ourselves.
+	 */
+	decoding_ctx = repack_setup_logical_decoding(shared->relid);
+
+	/* Announce that we're ready. */
+	SpinLockAcquire(&shared->mutex);
+	shared->initialized = true;
+	SpinLockRelease(&shared->mutex);
+	ConditionVariableSignal(&shared->cv);
+
+	/* Build the initial snapshot and export it. */
+	snapshot = SnapBuildInitialSnapshot(decoding_ctx->snapshot_builder, true);
+	export_initial_snapshot(snapshot, shared);
+
+	/*
+	 * Only historic snapshots should be used now. Do not let us restrict the
+	 * progress of xmin horizon.
+	 */
+	InvalidateCatalogSnapshot();
+
+	for (;;)
+	{
+		bool		stop = decode_concurrent_changes(decoding_ctx, shared);
+
+		if (stop)
+			break;
+
+	}
+
+	/* Cleanup. */
+	repack_cleanup_logical_decoding(decoding_ctx);
+	CommitTransactionCommand();
+}
+
+/*
+ * See ParallelWorkerShutdown for details.
+ */
+static void
+RepackWorkerShutdown(int code, Datum arg)
+{
+	SendProcSignal(backend_pid,
+				   PROCSIG_REPACK_MESSAGE,
+				   backend_proc_number);
+
+	dsm_detach((dsm_segment *) DatumGetPointer(arg));
+}
+
+bool
+IsRepackWorker(void)
+{
+	return is_repack_worker;
+}
+
+/*
+ * This function is much like pg_create_logical_replication_slot() except that
+ * the new slot is neither released (if anyone else could read changes from
+ * our slot, we could miss changes other backends do while we copy the
+ * existing data into temporary table), nor persisted (it's easier to handle
+ * crash by restarting all the work from scratch).
+ */
+static LogicalDecodingContext *
+repack_setup_logical_decoding(Oid relid)
+{
+	Relation	rel;
+	Oid			toastrelid;
+	LogicalDecodingContext *ctx;
+	NameData	slotname;
+	RepackDecodingState *dstate;
+	MemoryContext oldcxt;
+
+	/*
+	 * REPACK CONCURRENTLY is not allowed in a transaction block, so this
+	 * should never fire.
+	 */
+	Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
+
+	/*
+	 * Make sure we can use logical decoding.
+	 */
+	CheckSlotPermissions();
+	CheckLogicalDecodingRequirements();
+
+	/*
+	 * A single backend should not execute multiple REPACK commands at a time,
+	 * so use PID to make the slot unique.
+	 *
+	 * RS_TEMPORARY so that the slot gets cleaned up on ERROR.
+	 */
+	snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+	ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false,
+						  false);
+
+	EnsureLogicalDecodingEnabled();
+
+	/*
+	 * Neither prepare_write nor do_write callback nor update_progress is
+	 * useful for us.
+	 */
+	ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME,
+									NIL,
+									true,
+									InvalidXLogRecPtr,
+									XL_ROUTINE(.page_read = read_local_xlog_page,
+											   .segment_open = wal_segment_open,
+											   .segment_close = wal_segment_close),
+									NULL, NULL, NULL);
+
+	/*
+	 * We don't have control on setting fast_forward, so at least check it.
+	 */
+	Assert(!ctx->fast_forward);
+
+	/* Avoid logical decoding of other relations. */
+	rel = table_open(relid, AccessShareLock);
+	repacked_rel_locator = rel->rd_locator;
+	toastrelid = rel->rd_rel->reltoastrelid;
+	if (OidIsValid(toastrelid))
+	{
+		Relation	toastrel;
+
+		/* Avoid logical decoding of other TOAST relations. */
+		toastrel = table_open(toastrelid, AccessShareLock);
+		repacked_rel_toast_locator = toastrel->rd_locator;
+		table_close(toastrel, AccessShareLock);
+	}
+	table_close(rel, AccessShareLock);
+
+	DecodingContextFindStartpoint(ctx);
+
+	/*
+	 * decode_concurrent_changes() needs non-blocking callback.
+	 */
+	ctx->reader->routine.page_read = read_local_xlog_page_no_wait;
+
+	/* Some WAL records should have been read. */
+	Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+
+	/*
+	 * Initialize repack_current_segment so that we can notice WAL segment
+	 * boundaries.
+	 */
+	XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
+				wal_segment_size);
+
+	/* Our private state belongs to the decoding context. */
+	oldcxt = MemoryContextSwitchTo(ctx->context);
+
+	/*
+	 * read_local_xlog_page_no_wait() needs to be able to indicate the end of
+	 * WAL.
+	 */
+	ctx->reader->private_data = palloc0_object(ReadLocalXLogPageNoWaitPrivate);
+	dstate = palloc0_object(RepackDecodingState);
+	MemoryContextSwitchTo(oldcxt);
+
+#ifdef	USE_ASSERT_CHECKING
+	dstate->relid = relid;
+#endif
+
+	dstate->change_cxt = AllocSetContextCreate(ctx->context,
+											   "REPACK - change",
+											   ALLOCSET_DEFAULT_SIZES);
+
+	/* The file will be set as soon as we have it opened. */
+	dstate->file = NULL;
+
+	/*
+	 * Memory context and resource owner for long-lived resources.
+	 */
+	dstate->worker_cxt = CurrentMemoryContext;
+	dstate->worker_resowner = CurrentResourceOwner;
+
+	ctx->output_writer_private = dstate;
+
+	return ctx;
+}
+
+static void
+repack_cleanup_logical_decoding(LogicalDecodingContext *ctx)
+{
+	RepackDecodingState *dstate;
+
+	dstate = (RepackDecodingState *) ctx->output_writer_private;
+	if (dstate->slot)
+		ExecDropSingleTupleTableSlot(dstate->slot);
+
+	FreeDecodingContext(ctx);
+	ReplicationSlotDropAcquired();
+}
+
+/*
+ * Make snapshot available to the backend that launched the decoding worker.
+ */
+static void
+export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared)
+{
+	char		fname[MAXPGPATH];
+	BufFile    *file;
+	Size		snap_size;
+	char	   *snap_space;
+
+	snap_size = EstimateSnapshotSpace(snapshot);
+	snap_space = (char *) palloc(snap_size);
+	SerializeSnapshot(snapshot, snap_space);
+	FreeSnapshot(snapshot);
+
+	DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+	file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+	/* To make restoration easier, write the snapshot size first. */
+	BufFileWrite(file, &snap_size, sizeof(snap_size));
+	BufFileWrite(file, snap_space, snap_size);
+	pfree(snap_space);
+	BufFileClose(file);
+
+	/* Increase the counter to tell the backend that the file is available. */
+	SpinLockAcquire(&shared->mutex);
+	shared->last_exported++;
+	SpinLockRelease(&shared->mutex);
+	ConditionVariableSignal(&shared->cv);
+}
+
+/*
+ * Decode logical changes from the WAL sequence and store them to a file.
+ *
+ * If true is returned, there is no more work for the worker.
+ */
+static bool
+decode_concurrent_changes(LogicalDecodingContext *ctx,
+						  DecodingWorkerShared *shared)
+{
+	RepackDecodingState *dstate;
+	XLogRecPtr	lsn_upto;
+	bool		done;
+	char		fname[MAXPGPATH];
+
+	dstate = (RepackDecodingState *) ctx->output_writer_private;
+
+	/* Open the output file. */
+	DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1);
+	dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname);
+
+	SpinLockAcquire(&shared->mutex);
+	lsn_upto = shared->lsn_upto;
+	done = shared->done;
+	SpinLockRelease(&shared->mutex);
+
+	while (true)
+	{
+		XLogRecord *record;
+		XLogSegNo	segno_new;
+		char	   *errm = NULL;
+		XLogRecPtr	end_lsn;
+
+		CHECK_FOR_INTERRUPTS();
+
+		record = XLogReadRecord(ctx->reader, &errm);
+		if (record)
+		{
+			LogicalDecodingProcessRecord(ctx, ctx->reader);
+
+			/*
+			 * If WAL segment boundary has been crossed, inform the decoding
+			 * system that the catalog_xmin can advance.
+			 *
+			 * TODO Does it make sense to confirm more often? Segment size
+			 * seems appropriate for restart_lsn (because less than a segment
+			 * cannot be recycled anyway), however more frequent checks might
+			 * be beneficial for catalog_xmin.
+			 */
+			end_lsn = ctx->reader->EndRecPtr;
+			XLByteToSeg(end_lsn, segno_new, wal_segment_size);
+			if (segno_new != repack_current_segment)
+			{
+				LogicalConfirmReceivedLocation(end_lsn);
+				elog(DEBUG1, "REPACK: confirmed receive location %X/%X",
+					 (uint32) (end_lsn >> 32), (uint32) end_lsn);
+				repack_current_segment = segno_new;
+			}
+		}
+		else
+		{
+			ReadLocalXLogPageNoWaitPrivate *priv;
+
+			if (errm)
+				ereport(ERROR,
+						errmsg("%s", errm));
+
+			/*
+			 * In the decoding loop we do not want to get blocked when there
+			 * is no more WAL available, otherwise the loop would become
+			 * uninterruptible.
+			 */
+			priv = (ReadLocalXLogPageNoWaitPrivate *) ctx->reader->private_data;
+			if (priv->end_of_wal)
+				/* Do not miss the end of WAL condition next time. */
+				priv->end_of_wal = false;
+			else
+				ereport(ERROR,
+						errmsg("could not read WAL record"));
+		}
+
+		/*
+		 * Whether we could read new record or not, keep checking if
+		 * 'lsn_upto' was specified.
+		 */
+		if (!XLogRecPtrIsValid(lsn_upto))
+		{
+			SpinLockAcquire(&shared->mutex);
+			lsn_upto = shared->lsn_upto;
+			/* 'done' should be set at the same time as 'lsn_upto' */
+			done = shared->done;
+			SpinLockRelease(&shared->mutex);
+		}
+		if (XLogRecPtrIsValid(lsn_upto) &&
+			ctx->reader->EndRecPtr >= lsn_upto)
+			break;
+
+		if (record == NULL)
+		{
+			int64		timeout = 0;
+			WaitLSNResult res;
+
+			/*
+			 * Before we retry reading, wait until new WAL is flushed.
+			 *
+			 * There is a race condition such that the backend executing
+			 * REPACK determines 'lsn_upto', but before it sets the shared
+			 * variable, we reach the end of WAL. In that case we'd need to
+			 * wait until the next WAL flush (unrelated to REPACK). Although
+			 * that should not be a problem in a busy system, it might be
+			 * noticeable in other cases, including regression tests (which
+			 * are not necessarily executed in parallel). Therefore it makes
+			 * sense to use timeout.
+			 *
+			 * If lsn_upto is valid, WAL records having LSN lower than that
+			 * should already have been flushed to disk.
+			 */
+			if (!XLogRecPtrIsValid(lsn_upto))
+				timeout = 100L;
+			res = WaitForLSN(WAIT_LSN_TYPE_PRIMARY_FLUSH,
+							 ctx->reader->EndRecPtr + 1,
+							 timeout);
+			if (res != WAIT_LSN_RESULT_SUCCESS &&
+				res != WAIT_LSN_RESULT_TIMEOUT)
+				ereport(ERROR,
+						errmsg("waiting for WAL failed"));
+		}
+	}
+
+	/*
+	 * Close the file so we can make it available to the backend.
+	 */
+	BufFileClose(dstate->file);
+	dstate->file = NULL;
+	SpinLockAcquire(&shared->mutex);
+	shared->lsn_upto = InvalidXLogRecPtr;
+	shared->last_exported++;
+	SpinLockRelease(&shared->mutex);
+	ConditionVariableSignal(&shared->cv);
+
+	return done;
+}
+
+/*
+ * Does the WAL record contain a data change that this backend does not need
+ * to decode on behalf of REPACK (CONCURRENTLY)?
+ */
+bool
+change_useless_for_repack(XLogRecordBuffer *buf)
+{
+	XLogReaderState *r = buf->record;
+	RelFileLocator locator;
+
+	/* TOAST locator should not be set unless the main is. */
+	Assert(!OidIsValid(repacked_rel_toast_locator.relNumber) ||
+		   OidIsValid(repacked_rel_locator.relNumber));
+
+	/*
+	 * Backends not involved in REPACK (CONCURRENTLY) should not do the
+	 * filtering.
+	 */
+	if (!OidIsValid(repacked_rel_locator.relNumber))
+		return false;
+
+	/*
+	 * If the record does not contain the block 0, it's probably not INSERT /
+	 * UPDATE / DELETE. In any case, we do not have enough information to
+	 * filter the change out.
+	 */
+	if (!XLogRecGetBlockTagExtended(r, 0, &locator, NULL, NULL, NULL))
+		return false;
+
+	/*
+	 * Decode the change if it belongs to the table we are repacking, or if it
+	 * belongs to its TOAST relation.
+	 */
+	if (RelFileLocatorEquals(locator, repacked_rel_locator))
+		return false;
+	if (OidIsValid(repacked_rel_toast_locator.relNumber) &&
+		RelFileLocatorEquals(locator, repacked_rel_toast_locator))
+		return false;
+
+	/* Filter out changes of other tables. */
+	return true;
+}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c69c12dc014..cc70439627b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6058,6 +6058,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			finish_heap_swap(tab->relid, OIDNewHeap,
 							 false, false, true,
 							 !OidIsValid(tab->newTableSpace),
+							 true,	/* reindex */
 							 RecentXmin,
 							 ReadNextMultiXactId(),
 							 persistence);
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..201835f30a4 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -127,7 +127,7 @@ static void vac_truncate_clog(TransactionId frozenXID,
 							  TransactionId lastSaneFrozenXid,
 							  MultiXactId lastSaneMinMulti);
 static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
-					   BufferAccessStrategy bstrategy);
+					   BufferAccessStrategy bstrategy, bool isTopLevel);
 static double compute_parallel_delay(void);
 static VacOptValue get_vacoptval_from_boolean(DefElem *def);
 static bool vac_tid_reaped(ItemPointer itemptr, void *state);
@@ -630,7 +630,8 @@ vacuum(List *relations, const VacuumParams params, BufferAccessStrategy bstrateg
 
 			if (params.options & VACOPT_VACUUM)
 			{
-				if (!vacuum_rel(vrel->oid, vrel->relation, params, bstrategy))
+				if (!vacuum_rel(vrel->oid, vrel->relation, params, bstrategy,
+								isTopLevel))
 					continue;
 			}
 
@@ -2004,7 +2005,7 @@ vac_truncate_clog(TransactionId frozenXID,
  */
 static bool
 vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
-		   BufferAccessStrategy bstrategy)
+		   BufferAccessStrategy bstrategy, bool isTopLevel)
 {
 	LOCKMODE	lmode;
 	Relation	rel;
@@ -2295,7 +2296,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 
 			/* VACUUM FULL is a variant of REPACK; see cluster.c */
 			cluster_rel(REPACK_COMMAND_VACUUMFULL, rel, InvalidOid,
-						&cluster_params);
+						&cluster_params, isTopLevel);
 			/* cluster_rel closes the relation, but keeps lock */
 
 			rel = NULL;
@@ -2338,7 +2339,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 		toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
 		toast_vacuum_params.toast_parent = relid;
 
-		vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy);
+		vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy,
+				   isTopLevel);
 	}
 
 	/*
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 4cd5e262e0f..680c29f35d5 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -1522,14 +1522,17 @@ ExecDeleteAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 			  ItemPointer tupleid, bool changingPart)
 {
 	EState	   *estate = context->estate;
+	int			options = TABLE_DELETE_WAIT;
+
+	if (changingPart)
+		options |= TABLE_DELETE_CHANGING_PART;
 
 	return table_tuple_delete(resultRelInfo->ri_RelationDesc, tupleid,
 							  estate->es_output_cid,
 							  estate->es_snapshot,
 							  estate->es_crosscheck_snapshot,
-							  true /* wait for commit */ ,
-							  &context->tmfd,
-							  changingPart);
+							  options,
+							  &context->tmfd);
 }
 
 /*
@@ -2333,7 +2336,7 @@ lreplace:
 								estate->es_output_cid,
 								estate->es_snapshot,
 								estate->es_crosscheck_snapshot,
-								true /* wait for commit */ ,
+								TABLE_UPDATE_WAIT,
 								&context->tmfd, &updateCxt->lockmode,
 								&updateCxt->updateIndexes);
 
diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c
index 22e5164adbf..1000b7bb06e 100644
--- a/src/backend/libpq/pqmq.c
+++ b/src/backend/libpq/pqmq.c
@@ -14,6 +14,7 @@
 #include "postgres.h"
 
 #include "access/parallel.h"
+#include "commands/cluster.h"
 #include "libpq/libpq.h"
 #include "libpq/pqformat.h"
 #include "libpq/pqmq.h"
@@ -177,6 +178,10 @@ mq_putmessage(char msgtype, const char *s, size_t len)
 				SendProcSignal(pq_mq_parallel_leader_pid,
 							   PROCSIG_PARALLEL_APPLY_MESSAGE,
 							   pq_mq_parallel_leader_proc_number);
+			else if (IsRepackWorker())
+				SendProcSignal(pq_mq_parallel_leader_pid,
+							   PROCSIG_REPACK_MESSAGE,
+							   pq_mq_parallel_leader_proc_number);
 			else
 			{
 				Assert(IsParallelWorker());
diff --git a/src/backend/meson.build b/src/backend/meson.build
index 4f5292d8f88..5e3cfe2d6f8 100644
--- a/src/backend/meson.build
+++ b/src/backend/meson.build
@@ -219,5 +219,6 @@ pg_test_mod_args = pg_mod_args + {
 subdir('jit/llvm')
 subdir('replication/libpqwalreceiver')
 subdir('replication/pgoutput')
+subdir('replication/pgoutput_repack')
 subdir('snowball')
 subdir('utils/mb/conversion_procs')
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index d1fe3cc71ce..1de66cc038f 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -13,6 +13,7 @@
 #include "postgres.h"
 
 #include "access/parallel.h"
+#include "commands/cluster.h"
 #include "libpq/pqsignal.h"
 #include "miscadmin.h"
 #include "pgstat.h"
@@ -136,6 +137,10 @@ static const struct
 		.fn_name = "ParallelApplyWorkerMain",
 		.fn_addr = ParallelApplyWorkerMain
 	},
+	{
+		.fn_name = "RepackWorkerMain",
+		.fn_addr = RepackWorkerMain
+	},
 	{
 		.fn_name = "TableSyncWorkerMain",
 		.fn_addr = TableSyncWorkerMain
@@ -143,7 +148,7 @@ static const struct
 	{
 		.fn_name = "SequenceSyncWorkerMain",
 		.fn_addr = SequenceSyncWorkerMain
-	}
+	},
 };
 
 /* Private functions. */
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index 3c027bcb2f7..f172c0b2b40 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -33,6 +33,7 @@
 #include "access/xlogreader.h"
 #include "access/xlogrecord.h"
 #include "catalog/pg_control.h"
+#include "commands/cluster.h"
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/message.h"
@@ -420,7 +421,8 @@ heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 	{
 		case XLOG_HEAP2_MULTI_INSERT:
 			if (SnapBuildProcessChange(builder, xid, buf->origptr) &&
-				!ctx->fast_forward)
+				!ctx->fast_forward &&
+				!change_useless_for_repack(buf))
 				DecodeMultiInsert(ctx, buf);
 			break;
 		case XLOG_HEAP2_NEW_CID:
@@ -465,6 +467,15 @@ heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 	TransactionId xid = XLogRecGetXid(buf->record);
 	SnapBuild  *builder = ctx->snapshot_builder;
 
+	/*
+	 * XXX Should we return here if change_useless_for_repack() returns true,
+	 * instead of calling the function below? Unlike the fast-forward case, we
+	 * shouldn't need the base snapshot for the containing transaction until
+	 * we receive a change that belongs to the table being REPACKed. Thus it
+	 * should be fine to skip SnapBuildProcessChange(), and therefore
+	 * reorderbuffer.c can create the transaction later.
+	 */
+
 	ReorderBufferProcessXid(ctx->reorder, xid, buf->origptr);
 
 	/*
@@ -482,7 +493,8 @@ heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 	{
 		case XLOG_HEAP_INSERT:
 			if (SnapBuildProcessChange(builder, xid, buf->origptr) &&
-				!ctx->fast_forward)
+				!ctx->fast_forward &&
+				!change_useless_for_repack(buf))
 				DecodeInsert(ctx, buf);
 			break;
 
@@ -494,19 +506,22 @@ heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 		case XLOG_HEAP_HOT_UPDATE:
 		case XLOG_HEAP_UPDATE:
 			if (SnapBuildProcessChange(builder, xid, buf->origptr) &&
-				!ctx->fast_forward)
+				!ctx->fast_forward &&
+				!change_useless_for_repack(buf))
 				DecodeUpdate(ctx, buf);
 			break;
 
 		case XLOG_HEAP_DELETE:
 			if (SnapBuildProcessChange(builder, xid, buf->origptr) &&
-				!ctx->fast_forward)
+				!ctx->fast_forward &&
+				!change_useless_for_repack(buf))
 				DecodeDelete(ctx, buf);
 			break;
 
 		case XLOG_HEAP_TRUNCATE:
 			if (SnapBuildProcessChange(builder, xid, buf->origptr) &&
-				!ctx->fast_forward)
+				!ctx->fast_forward &&
+				!change_useless_for_repack(buf))
 				DecodeTruncate(ctx, buf);
 			break;
 
@@ -522,7 +537,8 @@ heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 
 		case XLOG_HEAP_CONFIRM:
 			if (SnapBuildProcessChange(builder, xid, buf->origptr) &&
-				!ctx->fast_forward)
+				!ctx->fast_forward &&
+				!change_useless_for_repack(buf))
 				DecodeSpecConfirm(ctx, buf);
 			break;
 
@@ -1019,6 +1035,15 @@ DecodeDelete(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 
 	xlrec = (xl_heap_delete *) XLogRecGetData(r);
 
+	/*
+	 * Ignore changes which are considered useless for logical decoding.
+	 * Currently such changes are created by REPACK (CONCURRENTLY) when
+	 * replays DELETE commands on the new table (which is not yet visible to
+	 * other transactions).
+	 */
+	if (xlrec->flags & XLH_DELETE_NO_LOGICAL)
+		return;
+
 	/* only interested in our database */
 	XLogRecGetBlockTag(r, 0, &target_locator, NULL, NULL);
 	if (target_locator.dbOid != ctx->slot->data.database)
diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c
index 603a2b94d05..7651b187418 100644
--- a/src/backend/replication/logical/logical.c
+++ b/src/backend/replication/logical/logical.c
@@ -194,7 +194,11 @@ StartupDecodingContext(List *output_plugin_options,
 
 	ctx->slot = slot;
 
-	ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, ctx);
+	/*
+	 * TODO A separate patch for PG core, unless there's really a reason to
+	 * pass ctx for private_data (May extensions expect ctx?).
+	 */
+	ctx->reader = XLogReaderAllocate(wal_segment_size, NULL, xl_routine, NULL);
 	if (!ctx->reader)
 		ereport(ERROR,
 				(errcode(ERRCODE_OUT_OF_MEMORY),
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 37f0c6028bd..9cf499ce7c6 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -440,7 +440,7 @@ SnapBuildBuildSnapshot(SnapBuild *builder)
  * for loading in different transaction.
  */
 Snapshot
-SnapBuildInitialSnapshot(SnapBuild *builder)
+SnapBuildInitialSnapshot(SnapBuild *builder, bool repack)
 {
 	Snapshot	snap;
 	TransactionId xid;
@@ -448,7 +448,7 @@ SnapBuildInitialSnapshot(SnapBuild *builder)
 	TransactionId *newxip;
 	int			newxcnt = 0;
 
-	Assert(XactIsoLevel == XACT_REPEATABLE_READ);
+	Assert(XactIsoLevel == XACT_REPEATABLE_READ || repack);
 	Assert(builder->building_full_snapshot);
 
 	/* don't allow older snapshots */
@@ -526,6 +526,11 @@ SnapBuildInitialSnapshot(SnapBuild *builder)
 	snap->xcnt = newxcnt;
 	snap->xip = newxip;
 
+	/*
+	 * FreeSnapshot() is more appropriate for REPACK than counting references.
+	 */
+	snap->copied = repack;
+
 	return snap;
 }
 
@@ -558,7 +563,7 @@ SnapBuildExportSnapshot(SnapBuild *builder)
 	XactIsoLevel = XACT_REPEATABLE_READ;
 	XactReadOnly = true;
 
-	snap = SnapBuildInitialSnapshot(builder);
+	snap = SnapBuildInitialSnapshot(builder, false);
 
 	/*
 	 * now that we've built a plain snapshot, make it active and use the
diff --git a/src/backend/replication/pgoutput_repack/Makefile b/src/backend/replication/pgoutput_repack/Makefile
new file mode 100644
index 00000000000..4efeb713b70
--- /dev/null
+++ b/src/backend/replication/pgoutput_repack/Makefile
@@ -0,0 +1,32 @@
+#-------------------------------------------------------------------------
+#
+# Makefile--
+#    Makefile for src/backend/replication/pgoutput_repack
+#
+# IDENTIFICATION
+#    src/backend/replication/pgoutput_repack
+#
+#-------------------------------------------------------------------------
+
+subdir = src/backend/replication/pgoutput_repack
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+
+OBJS = \
+	$(WIN32RES) \
+	pgoutput_repack.o
+PGFILEDESC = "pgoutput_repack - logical replication output plugin for REPACK command"
+NAME = pgoutput_repack
+
+all: all-shared-lib
+
+include $(top_srcdir)/src/Makefile.shlib
+
+install: all installdirs install-lib
+
+installdirs: installdirs-lib
+
+uninstall: uninstall-lib
+
+clean distclean: clean-lib
+	rm -f $(OBJS)
diff --git a/src/backend/replication/pgoutput_repack/meson.build b/src/backend/replication/pgoutput_repack/meson.build
new file mode 100644
index 00000000000..6a88c0fb08d
--- /dev/null
+++ b/src/backend/replication/pgoutput_repack/meson.build
@@ -0,0 +1,18 @@
+# Copyright (c) 2022-2026, PostgreSQL Global Development Group
+
+pgoutput_repack_sources = files(
+  'pgoutput_repack.c',
+)
+
+if host_system == 'windows'
+  pgoutput_repack_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'pgoutput_repack',
+    '--FILEDESC', 'pgoutput_repack - logical replication output plugin for REPACK command',])
+endif
+
+pgoutput_repack = shared_module('pgoutput_repack',
+  pgoutput_repack_sources,
+  kwargs: pg_mod_args,
+)
+
+backend_targets += pgoutput_repack
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
new file mode 100644
index 00000000000..bf8ee003305
--- /dev/null
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -0,0 +1,289 @@
+/*-------------------------------------------------------------------------
+ *
+ * pgoutput_repack.c
+ *		Logical Replication output plugin for REPACK command
+ *
+ * Copyright (c) 2012-2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/backend/replication/pgoutput_repack/pgoutput_repack.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/detoast.h"
+#include "commands/repack_internal.h"
+#include "replication/snapbuild.h"
+#include "utils/memutils.h"
+
+PG_MODULE_MAGIC;
+
+static void repack_startup(LogicalDecodingContext *ctx,
+						   OutputPluginOptions *opt, bool is_init);
+static void repack_shutdown(LogicalDecodingContext *ctx);
+static void repack_begin_txn(LogicalDecodingContext *ctx,
+							 ReorderBufferTXN *txn);
+static void repack_commit_txn(LogicalDecodingContext *ctx,
+							  ReorderBufferTXN *txn, XLogRecPtr commit_lsn);
+static void repack_process_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
+								  Relation rel, ReorderBufferChange *change);
+static void repack_store_change(LogicalDecodingContext *ctx, Relation relation,
+								ConcurrentChangeKind kind, HeapTuple tuple);
+
+void
+_PG_output_plugin_init(OutputPluginCallbacks *cb)
+{
+	cb->startup_cb = repack_startup;
+	cb->begin_cb = repack_begin_txn;
+	cb->change_cb = repack_process_change;
+	cb->commit_cb = repack_commit_txn;
+	cb->shutdown_cb = repack_shutdown;
+}
+
+
+/* initialize this plugin */
+static void
+repack_startup(LogicalDecodingContext *ctx, OutputPluginOptions *opt,
+			   bool is_init)
+{
+	ctx->output_plugin_private = NULL;
+
+	/* Probably unnecessary, as we don't use the SQL interface ... */
+	opt->output_type = OUTPUT_PLUGIN_BINARY_OUTPUT;
+
+	if (ctx->output_plugin_options != NIL)
+	{
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("this plugin does not expect any options"));
+	}
+}
+
+static void
+repack_shutdown(LogicalDecodingContext *ctx)
+{
+}
+
+/*
+ * As we don't release the slot during processing of particular table, there's
+ * no room for SQL interface, even for debugging purposes. Therefore we need
+ * neither OutputPluginPrepareWrite() nor OutputPluginWrite() in the plugin
+ * callbacks. (Although we might want to write custom callbacks, this API
+ * seems to be unnecessarily generic for our purposes.)
+ */
+
+/* BEGIN callback */
+static void
+repack_begin_txn(LogicalDecodingContext *ctx, ReorderBufferTXN *txn)
+{
+}
+
+/* COMMIT callback */
+static void
+repack_commit_txn(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
+				  XLogRecPtr commit_lsn)
+{
+}
+
+/*
+ * Callback for individual changed tuples
+ */
+static void
+repack_process_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
+					  Relation relation, ReorderBufferChange *change)
+{
+	RepackDecodingState *private = (RepackDecodingState *) ctx->output_writer_private;
+
+	/* Changes of other relation should not have been decoded. */
+	Assert(RelationGetRelid(relation) == private->relid);
+
+	/* Decode entry depending on its type */
+	switch (change->action)
+	{
+		case REORDER_BUFFER_CHANGE_INSERT:
+			{
+				HeapTuple	newtuple;
+
+				newtuple = change->data.tp.newtuple;
+
+				/*
+				 * Identity checks in the main function should have made this
+				 * impossible.
+				 */
+				if (newtuple == NULL)
+					elog(ERROR, "incomplete insert info.");
+
+				repack_store_change(ctx, relation, CHANGE_INSERT, newtuple);
+			}
+			break;
+		case REORDER_BUFFER_CHANGE_UPDATE:
+			{
+				HeapTuple	oldtuple,
+							newtuple;
+
+				oldtuple = change->data.tp.oldtuple;
+				newtuple = change->data.tp.newtuple;
+
+				if (newtuple == NULL)
+					elog(ERROR, "incomplete update info.");
+
+				if (oldtuple != NULL)
+					repack_store_change(ctx, relation, CHANGE_UPDATE_OLD, oldtuple);
+
+				repack_store_change(ctx, relation, CHANGE_UPDATE_NEW, newtuple);
+			}
+			break;
+		case REORDER_BUFFER_CHANGE_DELETE:
+			{
+				HeapTuple	oldtuple;
+
+				oldtuple = change->data.tp.oldtuple;
+
+				if (oldtuple == NULL)
+					elog(ERROR, "incomplete delete info.");
+
+				repack_store_change(ctx, relation, CHANGE_DELETE, oldtuple);
+			}
+			break;
+		default:
+
+			/*
+			 * Should not come here. This includes TRUNCATE of the table being
+			 * processed. heap_decode() cannot check the file locator easily,
+			 * but we assume that TRUNCATE uses AccessExclusiveLock on the
+			 * table so it should not occur during REPACK (CONCURRENTLY).
+			 */
+			Assert(false);
+			break;
+	}
+}
+
+/*
+ * Write the given tuple, with the given change kind, to the repack spill
+ * file.  Later, the repack decoding worker can read these and replay
+ * the operations on the new copy of the table.
+ *
+ * For each change affecting the table being repacked, we store enough
+ * information about each tuple in it, so that it can be replayed in the
+ * new copy of the table.
+ *
+ * XXX for DELETE and the UPDATE OLD tuples, we could store just the
+ * replication identity instead of the full tuple.
+ */
+static void
+repack_store_change(LogicalDecodingContext *ctx, Relation relation,
+					ConcurrentChangeKind kind, HeapTuple tuple)
+{
+	RepackDecodingState *dstate;
+	MemoryContext oldcxt;
+	BufFile    *file;
+	List	   *attrs_ext = NIL;
+	int			natt_ext;
+
+	dstate = (RepackDecodingState *) ctx->output_writer_private;
+	file = dstate->file;
+
+	/* Store the change kind. */
+	BufFileWrite(file, &kind, 1);
+
+	/* Use a frequently-reset context to avoid dealing with leaks manually */
+	oldcxt = MemoryContextSwitchTo(dstate->change_cxt);
+
+	/*
+	 * If the tuple contains "external indirect" attributes, we need to write
+	 * the contents to the file because we have no control over that memory.
+	 */
+	if (HeapTupleHasExternal(tuple))
+	{
+		TupleDesc	desc = RelationGetDescr(relation);
+		TupleTableSlot *slot;
+
+		/* Initialize the slot, if not done already */
+		if (dstate->slot == NULL)
+		{
+			ResourceOwner saveResourceOwner;
+
+			MemoryContextSwitchTo(dstate->worker_cxt);
+			saveResourceOwner = CurrentResourceOwner;
+			CurrentResourceOwner = dstate->worker_resowner;
+			dstate->slot = MakeSingleTupleTableSlot(desc, &TTSOpsHeapTuple);
+			MemoryContextSwitchTo(dstate->change_cxt);
+			CurrentResourceOwner = saveResourceOwner;
+		}
+
+		slot = dstate->slot;
+		ExecStoreHeapTuple(tuple, slot, false);
+
+		/*
+		 * Loop over all attributes, and find out which ones we need to spill
+		 * separately, to wit: each one that's a non-null varlena and stored
+		 * out of line.
+		 */
+		for (int i = 0; i < desc->natts; i++)
+		{
+			CompactAttribute *attr = TupleDescCompactAttr(desc, i);
+			varlena    *varlen;
+
+			if (attr->attisdropped || attr->attlen != -1 ||
+				slot_attisnull(slot, i + 1))
+				continue;
+
+			slot_getsomeattrs(slot, i + 1);
+
+			/*
+			 * This is a non-null varlena datum, but we only care if it's
+			 * out-of-line
+			 */
+			varlen = (varlena *) DatumGetPointer(slot->tts_values[i]);
+			if (!VARATT_IS_EXTERNAL(varlen))
+				continue;
+
+			/*
+			 * We spill any indirect-external attributes separately from the
+			 * heap tuple.  Anything else is written as is.
+			 */
+			if (VARATT_IS_EXTERNAL_INDIRECT(varlen))
+				attrs_ext = lappend(attrs_ext, varlen);
+			else
+			{
+				/*
+				 * Logical decoding should not produce "external expanded"
+				 * attributes (those actually should never appear on disk), so
+				 * only TOASTed attribute can be seen here.
+				 *
+				 * We get here if the table has external values but only
+				 * in-line values are being updated now.
+				 */
+				Assert(VARATT_IS_EXTERNAL_ONDISK(varlen));
+			}
+		}
+
+		ExecClearTuple(slot);
+	}
+
+	/*
+	 * First, write the original heap tuple, prefixed by its length.  Note
+	 * that the external-toast tag for each toasted attribute will be present
+	 * in what we write, so that we know where to restore each one later.
+	 */
+	BufFileWrite(file, &tuple->t_len, sizeof(tuple->t_len));
+	BufFileWrite(file, tuple->t_data, tuple->t_len);
+
+	/* Then, write the number of external attributes we found. */
+	natt_ext = list_length(attrs_ext);
+	BufFileWrite(file, &natt_ext, sizeof(natt_ext));
+
+	/* Finally, the attributes themselves, if any */
+	foreach_ptr(varlena, attr_val, attrs_ext)
+	{
+		attr_val = detoast_external_attr(attr_val);
+		BufFileWrite(file, attr_val, VARSIZE_ANY(attr_val));
+		/* These attributes could be large, so free them right away */
+		pfree(attr_val);
+	}
+
+	/* Cleanup. */
+	MemoryContextSwitchTo(oldcxt);
+	MemoryContextReset(dstate->change_cxt);
+}
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 66507e9c2dd..fbbe09135bf 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -1342,7 +1342,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd)
 		{
 			Snapshot	snap;
 
-			snap = SnapBuildInitialSnapshot(ctx->snapshot_builder);
+			snap = SnapBuildInitialSnapshot(ctx->snapshot_builder, false);
 			RestoreTransactionSnapshot(snap, MyProc);
 		}
 
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 7e017c8d53b..dd980145ced 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -19,6 +19,7 @@
 
 #include "access/parallel.h"
 #include "commands/async.h"
+#include "commands/cluster.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "port/pg_bitutils.h"
@@ -700,6 +701,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS)
 	if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE))
 		HandleParallelApplyMessageInterrupt();
 
+	if (CheckProcSignal(PROCSIG_REPACK_MESSAGE))
+		HandleRepackMessageInterrupt();
+
 	if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT))
 		HandleRecoveryConflictInterrupt();
 
diff --git a/src/backend/storage/lmgr/generate-lwlocknames.pl b/src/backend/storage/lmgr/generate-lwlocknames.pl
index b49007167b0..2e7f1054e62 100644
--- a/src/backend/storage/lmgr/generate-lwlocknames.pl
+++ b/src/backend/storage/lmgr/generate-lwlocknames.pl
@@ -162,7 +162,7 @@ while (<$lwlocklist>)
 
 die
   "$wait_event_lwlocks[$lwlock_count] defined in wait_event_names.txt but "
-  . " missing from lwlocklist.h"
+  . "missing from lwlocklist.h"
   if $lwlock_count < scalar @wait_event_lwlocks;
 
 die
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index b3563113219..4d253eddfa0 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -36,6 +36,7 @@
 #include "access/xact.h"
 #include "catalog/pg_type.h"
 #include "commands/async.h"
+#include "commands/cluster.h"
 #include "commands/event_trigger.h"
 #include "commands/explain_state.h"
 #include "commands/prepare.h"
@@ -3576,6 +3577,9 @@ ProcessInterrupts(void)
 
 	if (ParallelApplyMessagePending)
 		ProcessParallelApplyMessages();
+
+	if (RepackMessagePending)
+		ProcessRepackMessages();
 }
 
 /*
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 6be80d2daad..e2f21349997 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -154,6 +154,7 @@ RECOVERY_CONFLICT_SNAPSHOT	"Waiting for recovery conflict resolution for a vacuu
 RECOVERY_CONFLICT_TABLESPACE	"Waiting for recovery conflict resolution for dropping a tablespace."
 RECOVERY_END_COMMAND	"Waiting for <xref linkend="guc-recovery-end-command"/> to complete."
 RECOVERY_PAUSE	"Waiting for recovery to be resumed."
+REPACK_WORKER_EXPORT	"Waiting for decoding worker to export a new output file."
 REPLICATION_ORIGIN_DROP	"Waiting for a replication origin to become inactive so it can be dropped."
 REPLICATION_SLOT_DROP	"Waiting for a replication slot to become inactive so it can be dropped."
 RESTORE_COMMAND	"Waiting for <xref linkend="guc-restore-command"/> to complete."
diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c
index 2e6197f5f35..e0129df3e40 100644
--- a/src/backend/utils/time/snapmgr.c
+++ b/src/backend/utils/time/snapmgr.c
@@ -215,7 +215,6 @@ static List *exportedSnapshots = NIL;
 /* Prototypes for local functions */
 static Snapshot CopySnapshot(Snapshot snapshot);
 static void UnregisterSnapshotNoOwner(Snapshot snapshot);
-static void FreeSnapshot(Snapshot snapshot);
 static void SnapshotResetXmin(void);
 
 /* ResourceOwner callbacks to track snapshot references */
@@ -660,7 +659,7 @@ CopySnapshot(Snapshot snapshot)
  * FreeSnapshot
  *		Free the memory associated with a snapshot.
  */
-static void
+void
 FreeSnapshot(Snapshot snapshot)
 {
 	Assert(snapshot->regd_count == 0);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 523d3f39fc5..e41c965a1cc 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -5227,8 +5227,8 @@ match_previous_words(int pattern_id,
 		 * one word, so the above test is correct.
 		 */
 		if (ends_with(prev_wd, '(') || ends_with(prev_wd, ','))
-			COMPLETE_WITH("ANALYZE", "VERBOSE");
-		else if (TailMatches("ANALYZE", "VERBOSE"))
+			COMPLETE_WITH("ANALYZE", "CONCURRENTLY", "VERBOSE");
+		else if (TailMatches("ANALYZE", "CONCURRENTLY", "VERBOSE"))
 			COMPLETE_WITH("ON", "OFF");
 	}
 
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 9b403203006..67d113de10b 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -384,13 +384,13 @@ extern void heap_multi_insert(Relation relation, TupleTableSlot **slots,
 							  int ntuples, CommandId cid, int options,
 							  BulkInsertState bistate);
 extern TM_Result heap_delete(Relation relation, const ItemPointerData *tid,
-							 CommandId cid, Snapshot crosscheck, bool wait,
-							 TM_FailureData *tmfd, bool changingPart);
+							 CommandId cid, Snapshot crosscheck, int options,
+							 TM_FailureData *tmfd);
 extern void heap_finish_speculative(Relation relation, const ItemPointerData *tid);
 extern void heap_abort_speculative(Relation relation, const ItemPointerData *tid);
 extern TM_Result heap_update(Relation relation, const ItemPointerData *otid,
 							 HeapTuple newtup,
-							 CommandId cid, Snapshot crosscheck, bool wait,
+							 CommandId cid, Snapshot crosscheck, int options,
 							 TM_FailureData *tmfd, LockTupleMode *lockmode,
 							 TU_UpdateIndexes *update_indexes);
 extern TM_Result heap_lock_tuple(Relation relation, HeapTuple tuple,
diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h
index 516806fcca2..fdca7d821c8 100644
--- a/src/include/access/heapam_xlog.h
+++ b/src/include/access/heapam_xlog.h
@@ -104,6 +104,8 @@
 #define XLH_DELETE_CONTAINS_OLD_KEY				(1<<2)
 #define XLH_DELETE_IS_SUPER						(1<<3)
 #define XLH_DELETE_IS_PARTITION_MOVE			(1<<4)
+/* See heap_delete() */
+#define XLH_DELETE_NO_LOGICAL					(1<<5)
 
 /* convenience macro for checking whether any form of old tuple was logged */
 #define XLH_DELETE_CONTAINS_OLD						\
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 06084752245..1e51e22344f 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -260,6 +260,15 @@ typedef struct TM_IndexDeleteOp
 #define TABLE_INSERT_FROZEN			0x0004
 #define TABLE_INSERT_NO_LOGICAL		0x0008
 
+/* "options" flag bits for table_tuple_update */
+#define TABLE_UPDATE_WAIT			0x0001
+#define TABLE_UPDATE_NO_LOGICAL		0x0002
+
+/* "options" flag bits for table_tuple_delete */
+#define TABLE_DELETE_WAIT			0x0001
+#define TABLE_DELETE_CHANGING_PART	0x0002
+#define TABLE_DELETE_NO_LOGICAL		0x0004
+
 /* flag bits for table_tuple_lock */
 /* Follow tuples whose update is in progress if lock modes don't conflict  */
 #define TUPLE_LOCK_FLAG_LOCK_UPDATE_IN_PROGRESS	(1 << 0)
@@ -535,9 +544,8 @@ typedef struct TableAmRoutine
 								 CommandId cid,
 								 Snapshot snapshot,
 								 Snapshot crosscheck,
-								 bool wait,
-								 TM_FailureData *tmfd,
-								 bool changingPart);
+								 int options,
+								 TM_FailureData *tmfd);
 
 	/* see table_tuple_update() for reference about parameters */
 	TM_Result	(*tuple_update) (Relation rel,
@@ -546,7 +554,7 @@ typedef struct TableAmRoutine
 								 CommandId cid,
 								 Snapshot snapshot,
 								 Snapshot crosscheck,
-								 bool wait,
+								 int options,
 								 TM_FailureData *tmfd,
 								 LockTupleMode *lockmode,
 								 TU_UpdateIndexes *update_indexes);
@@ -629,6 +637,7 @@ typedef struct TableAmRoutine
 											  Relation OldIndex,
 											  bool use_sort,
 											  TransactionId OldestXmin,
+											  Snapshot snapshot,
 											  TransactionId *xid_cutoff,
 											  MultiXactId *multi_cutoff,
 											  double *num_tuples,
@@ -1459,6 +1468,7 @@ table_multi_insert(Relation rel, TupleTableSlot **slots, int nslots,
  *	cid - delete command ID (used for visibility test, and stored into
  *		cmax if successful)
  *	crosscheck - if not InvalidSnapshot, also check tuple against this
+ *	XXX document options
  *	wait - true if should wait for any conflicting update to commit/abort
  *	changingPart - true iff the tuple is being moved to another partition
  *		table due to an update of the partition key. Otherwise, false.
@@ -1476,12 +1486,12 @@ table_multi_insert(Relation rel, TupleTableSlot **slots, int nslots,
  */
 static inline TM_Result
 table_tuple_delete(Relation rel, ItemPointer tid, CommandId cid,
-				   Snapshot snapshot, Snapshot crosscheck, bool wait,
-				   TM_FailureData *tmfd, bool changingPart)
+				   Snapshot snapshot, Snapshot crosscheck, int options,
+				   TM_FailureData *tmfd)
 {
 	return rel->rd_tableam->tuple_delete(rel, tid, cid,
 										 snapshot, crosscheck,
-										 wait, tmfd, changingPart);
+										 options, tmfd);
 }
 
 /*
@@ -1496,7 +1506,12 @@ table_tuple_delete(Relation rel, ItemPointer tid, CommandId cid,
  *	cid - update command ID (used for visibility test, and stored into
  *		cmax/cmin if successful)
  *	crosscheck - if not InvalidSnapshot, also check old tuple against this
- *	wait - true if should wait for any conflicting update to commit/abort
+ *	options - These allow the caller to specify options that may change the
+ *	behavior of the AM. The AM will ignore options that it does not support.
+ *		TABLE_UPDATE_WAIT -- set if should wait for any conflicting update to
+ *		commit/abort
+ *		TABLE_UPDATE_NO_LOGICAL -- force-disables the emitting of logical
+ *		decoding information for the tuple.
  *
  * Output parameters:
  *	slot - newly constructed tuple data to store
@@ -1522,12 +1537,12 @@ table_tuple_delete(Relation rel, ItemPointer tid, CommandId cid,
 static inline TM_Result
 table_tuple_update(Relation rel, ItemPointer otid, TupleTableSlot *slot,
 				   CommandId cid, Snapshot snapshot, Snapshot crosscheck,
-				   bool wait, TM_FailureData *tmfd, LockTupleMode *lockmode,
+				   int options, TM_FailureData *tmfd, LockTupleMode *lockmode,
 				   TU_UpdateIndexes *update_indexes)
 {
 	return rel->rd_tableam->tuple_update(rel, otid, slot,
 										 cid, snapshot, crosscheck,
-										 wait, tmfd,
+										 options, tmfd,
 										 lockmode, update_indexes);
 }
 
@@ -1657,6 +1672,8 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
  *   not needed for the relation's AM
  * - *xid_cutoff - ditto
  * - *multi_cutoff - ditto
+ * - snapshot - if != NULL, ignore data changes done by transactions that this
+ *	 (MVCC) snapshot considers still in-progress or in the future.
  *
  * Output parameters:
  * - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1669,6 +1686,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
 								Relation OldIndex,
 								bool use_sort,
 								TransactionId OldestXmin,
+								Snapshot snapshot,
 								TransactionId *xid_cutoff,
 								MultiXactId *multi_cutoff,
 								double *num_tuples,
@@ -1677,6 +1695,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
 {
 	OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
 													use_sort, OldestXmin,
+													snapshot,
 													xid_cutoff, multi_cutoff,
 													num_tuples, tups_vacuumed,
 													tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index c79cd2d0e19..8ed7816e7b4 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -13,6 +13,8 @@
 #ifndef CLUSTER_H
 #define CLUSTER_H
 
+#include <signal.h>
+
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
 #include "storage/lockdefs.h"
@@ -25,6 +27,7 @@
 #define CLUOPT_RECHECK_ISCLUSTERED 0x04 /* recheck relation state for
 										 * indisclustered */
 #define CLUOPT_ANALYZE 0x08		/* do an ANALYZE */
+#define CLUOPT_CONCURRENT 0x10	/* allow concurrent data changes */
 
 /* options for CLUSTER */
 typedef struct ClusterParams
@@ -32,11 +35,13 @@ typedef struct ClusterParams
 	bits32		options;		/* bitmask of CLUOPT_* */
 } ClusterParams;
 
+extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending;
+
 
 extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
 
 extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
-						ClusterParams *params);
+						ClusterParams *params, bool isTopLevel);
 extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 									   LOCKMODE lockmode);
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
@@ -48,8 +53,17 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 							 bool swap_toast_by_content,
 							 bool check_constraints,
 							 bool is_internal,
+							 bool reindex,
 							 TransactionId frozenXid,
 							 MultiXactId cutoffMulti,
 							 char newrelpersistence);
 
+extern void HandleRepackMessageInterrupt(void);
+extern void ProcessRepackMessages(void);
+
+/* in repack_worker.c */
+extern void RepackWorkerMain(Datum main_arg);
+extern bool IsRepackWorker(void);
+
+
 #endif							/* CLUSTER_H */
diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h
index 9c40772706c..b5b01c1bb6d 100644
--- a/src/include/commands/progress.h
+++ b/src/include/commands/progress.h
@@ -86,10 +86,12 @@
 #define PROGRESS_REPACK_PHASE					1
 #define PROGRESS_REPACK_INDEX_RELID				2
 #define PROGRESS_REPACK_HEAP_TUPLES_SCANNED		3
-#define PROGRESS_REPACK_HEAP_TUPLES_WRITTEN		4
-#define PROGRESS_REPACK_TOTAL_HEAP_BLKS			5
-#define PROGRESS_REPACK_HEAP_BLKS_SCANNED		6
-#define PROGRESS_REPACK_INDEX_REBUILD_COUNT		7
+#define PROGRESS_REPACK_HEAP_TUPLES_INSERTED	4
+#define PROGRESS_REPACK_HEAP_TUPLES_UPDATED		5
+#define PROGRESS_REPACK_HEAP_TUPLES_DELETED		6
+#define PROGRESS_REPACK_TOTAL_HEAP_BLKS			7
+#define PROGRESS_REPACK_HEAP_BLKS_SCANNED		8
+#define PROGRESS_REPACK_INDEX_REBUILD_COUNT		9
 
 /*
  * Phases of repack (as advertised via PROGRESS_REPACK_PHASE).
@@ -98,9 +100,10 @@
 #define PROGRESS_REPACK_PHASE_INDEX_SCAN_HEAP	2
 #define PROGRESS_REPACK_PHASE_SORT_TUPLES		3
 #define PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP	4
-#define PROGRESS_REPACK_PHASE_SWAP_REL_FILES	5
-#define PROGRESS_REPACK_PHASE_REBUILD_INDEX		6
-#define PROGRESS_REPACK_PHASE_FINAL_CLEANUP		7
+#define PROGRESS_REPACK_PHASE_CATCH_UP			5
+#define PROGRESS_REPACK_PHASE_SWAP_REL_FILES	6
+#define PROGRESS_REPACK_PHASE_REBUILD_INDEX		7
+#define PROGRESS_REPACK_PHASE_FINAL_CLEANUP		8
 
 /* Progress parameters for CREATE INDEX */
 /* 3, 4 and 5 reserved for "waitfor" metrics */
diff --git a/src/include/commands/repack_internal.h b/src/include/commands/repack_internal.h
new file mode 100644
index 00000000000..d3402e0dedd
--- /dev/null
+++ b/src/include/commands/repack_internal.h
@@ -0,0 +1,128 @@
+/*-------------------------------------------------------------------------
+ *
+ * repack_internal.h
+ *	  header for REPACK internals
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994-5, Regents of the University of California
+ *
+ * src/include/commands/repack_internal.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef REPACK_INTERNAL_H
+#define REPACK_INTERNAL_H
+
+#include "nodes/execnodes.h"
+#include "replication/decode.h"
+#include "postmaster/bgworker.h"
+#include "replication/logical.h"
+#include "storage/buffile.h"
+#include "storage/sharedfileset.h"
+#include "storage/shm_mq.h"
+#include "utils/resowner.h"
+
+/*
+ * Stored as a single byte in the output file.
+ */
+typedef char ConcurrentChangeKind;
+
+#define		CHANGE_INSERT		'i'
+#define		CHANGE_UPDATE_OLD	'u'
+#define		CHANGE_UPDATE_NEW	'U'
+#define		CHANGE_DELETE		'd'
+
+extern pid_t backend_pid;
+extern ProcNumber backend_proc_number;
+
+/*
+ * Logical decoding state.
+ *
+ * The output plugin uses it to store the data changes that it decodes from
+ * WAL while the table contents is being copied to a new storage.
+ */
+typedef struct RepackDecodingState
+{
+#ifdef	USE_ASSERT_CHECKING
+	/* The relation whose changes we're decoding. */
+	Oid			relid;
+#endif
+
+	/* Per-change memory context. */
+	MemoryContext change_cxt;
+
+	/* A tuple slot used to pass tuples back and forth */
+	TupleTableSlot *slot;
+
+	/*
+	 * Memory context and resource owner of the decoding worker's transaction.
+	 */
+	MemoryContext	worker_cxt;
+	ResourceOwner	worker_resowner;
+
+	/* The current output file. */
+	BufFile    *file;
+} RepackDecodingState;
+
+/*
+ * Layout of shared memory used for communication between backend and the
+ * worker that performs logical decoding of data changes
+ */
+typedef struct DecodingWorkerShared
+{
+	/* Is the decoding initialized? */
+	bool		initialized;
+
+	/*
+	 * Once the worker has reached this LSN, it should close the current
+	 * output file and either create a new one or exit, according to the field
+	 * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all
+	 * the WAL available and keep checking this field. It is ok if the worker
+	 * had already decoded records whose LSN is >= lsn_upto before this field
+	 * has been set.
+	 */
+	XLogRecPtr	lsn_upto;
+
+	/* Exit after closing the current file? */
+	bool		done;
+
+	/* The output is stored here. */
+	SharedFileSet sfs;
+
+	/* Number of the last file exported by the worker. */
+	int			last_exported;
+
+	/* Synchronize access to the fields above. */
+	slock_t		mutex;
+
+	/* Database to connect to. */
+	Oid			dbid;
+
+	/* Role to connect as. */
+	Oid			roleid;
+
+	/* Decode data changes of this relation. */
+	Oid			relid;
+
+	/* The backend uses this to wait for the worker. */
+	ConditionVariable cv;
+
+	/* Info to signal the backend. */
+	PGPROC	   *backend_proc;
+	pid_t		backend_pid;
+	ProcNumber	backend_proc_number;
+
+	/*
+	 * Memory the queue is located in.
+	 *
+	 * For considerations on the value see the comments of
+	 * PARALLEL_ERROR_QUEUE_SIZE.
+	 */
+#define REPACK_ERROR_QUEUE_SIZE			16384
+	char		error_queue[FLEXIBLE_ARRAY_MEMBER];
+} DecodingWorkerShared;
+
+extern void DecodingWorkerFileName(char *fname, Oid relid, uint32 seq);
+
+
+#endif							/* REPACK_INTERNAL_H */
diff --git a/src/include/replication/decode.h b/src/include/replication/decode.h
index 49f00fc48b8..02b5393474c 100644
--- a/src/include/replication/decode.h
+++ b/src/include/replication/decode.h
@@ -31,4 +31,8 @@ extern void logicalmsg_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf
 extern void LogicalDecodingProcessRecord(LogicalDecodingContext *ctx,
 										 XLogReaderState *record);
 
+/* in commands/cluster.c */
+extern bool change_useless_for_repack(XLogRecordBuffer *buf);
+
+
 #endif
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index ccded021433..2b84f0058f0 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -72,7 +72,7 @@ extern void FreeSnapshotBuilder(SnapBuild *builder);
 
 extern void SnapBuildSnapDecRefcount(Snapshot snap);
 
-extern Snapshot SnapBuildInitialSnapshot(SnapBuild *builder);
+extern Snapshot SnapBuildInitialSnapshot(SnapBuild *builder, bool repack);
 extern const char *SnapBuildExportSnapshot(SnapBuild *builder);
 extern void SnapBuildClearExportedSnapshot(void);
 extern void SnapBuildResetExportedSnapshotState(void);
diff --git a/src/include/storage/lockdefs.h b/src/include/storage/lockdefs.h
index b73bb5618e6..3785b009808 100644
--- a/src/include/storage/lockdefs.h
+++ b/src/include/storage/lockdefs.h
@@ -36,8 +36,8 @@ typedef int LOCKMODE;
 #define AccessShareLock			1	/* SELECT */
 #define RowShareLock			2	/* SELECT FOR UPDATE/FOR SHARE */
 #define RowExclusiveLock		3	/* INSERT, UPDATE, DELETE */
-#define ShareUpdateExclusiveLock 4	/* VACUUM (non-FULL), ANALYZE, CREATE
-									 * INDEX CONCURRENTLY */
+#define ShareUpdateExclusiveLock 4	/* VACUUM (non-exclusive), ANALYZE, CREATE
+									 * INDEX CONCURRENTLY, REPACK CONCURRENTLY */
 #define ShareLock				5	/* CREATE INDEX (WITHOUT CONCURRENTLY) */
 #define ShareRowExclusiveLock	6	/* like EXCLUSIVE MODE, but allows ROW
 									 * SHARE */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index 348fba53a93..a944ee0d211 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -36,6 +36,7 @@ typedef enum
 	PROCSIG_BARRIER,			/* global barrier interrupt  */
 	PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */
 	PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */
+	PROCSIG_REPACK_MESSAGE,		/* Message from repack worker */
 	PROCSIG_RECOVERY_CONFLICT,	/* backend is blocking recovery, check
 								 * PGPROC->pendingRecoveryConflicts for the
 								 * reason */
diff --git a/src/include/utils/snapmgr.h b/src/include/utils/snapmgr.h
index 1c550096393..55ecc2c2054 100644
--- a/src/include/utils/snapmgr.h
+++ b/src/include/utils/snapmgr.h
@@ -78,6 +78,8 @@ extern Snapshot GetTransactionSnapshot(void);
 extern Snapshot GetLatestSnapshot(void);
 extern void SnapshotSetCommandId(CommandId curcid);
 
+extern void FreeSnapshot(Snapshot snapshot);
+
 extern Snapshot GetCatalogSnapshot(Oid relid);
 extern Snapshot GetNonHistoricCatalogSnapshot(Oid relid);
 extern void InvalidateCatalogSnapshot(void);
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index a41d781f8c9..2cd7d87c533 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -14,6 +14,8 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 
 ISOLATION = basic \
 	    inplace \
+	    repack \
+	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
 
diff --git a/src/test/modules/injection_points/expected/repack.out b/src/test/modules/injection_points/expected/repack.out
new file mode 100644
index 00000000000..b575e9052ee
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack.out
@@ -0,0 +1,113 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock change_existing change_new change_subxact1 change_subxact2 check2 wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_test USING INDEX repack_test_pkey;
+ <waiting ...>
+step change_existing: 
+	UPDATE repack_test SET i=10 where i=1;
+	UPDATE repack_test SET j=20 where i=2;
+	UPDATE repack_test SET i=30 where i=3;
+	UPDATE repack_test SET i=40 where i=30;
+	DELETE FROM repack_test WHERE i=4;
+
+step change_new: 
+	INSERT INTO repack_test(i, j) VALUES (5, 5), (6, 6), (7, 7), (8, 8);
+	UPDATE repack_test SET i=50 where i=5;
+	UPDATE repack_test SET j=60 where i=6;
+	DELETE FROM repack_test WHERE i=7;
+
+step change_subxact1: 
+	BEGIN;
+	INSERT INTO repack_test(i, j) VALUES (100, 100);
+	SAVEPOINT s1;
+	UPDATE repack_test SET i=101 where i=100;
+	SAVEPOINT s2;
+	UPDATE repack_test SET i=102 where i=101;
+	COMMIT;
+
+step change_subxact2: 
+	BEGIN;
+	SAVEPOINT s1;
+	INSERT INTO repack_test(i, j) VALUES (110, 110);
+	ROLLBACK TO SAVEPOINT s1;
+	INSERT INTO repack_test(i, j) VALUES (110, 111);
+	COMMIT;
+
+step check2: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_test';
+
+	SELECT i, j FROM repack_test ORDER BY i, j;
+
+	INSERT INTO data_s2(i, j)
+	SELECT i, j FROM repack_test;
+
+  i|  j
+---+---
+  2| 20
+  6| 60
+  8|  8
+ 10|  1
+ 40|  3
+ 50|  5
+102|100
+110|111
+(8 rows)
+
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_test';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_test ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_test;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    2
+(1 row)
+
+  i|  j
+---+---
+  2| 20
+  6| 60
+  8|  8
+ 10|  1
+ 40|  3
+ 50|  5
+102|100
+110|111
+(8 rows)
+
+count
+-----
+    0
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/expected/repack_toast.out b/src/test/modules/injection_points/expected/repack_toast.out
new file mode 100644
index 00000000000..b56dde134f8
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_toast.out
@@ -0,0 +1,65 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock change check2 wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_test;
+ <waiting ...>
+step change: 
+	UPDATE repack_test SET j=get_long_string() where i=2;
+	DELETE FROM repack_test WHERE i=3;
+	INSERT INTO repack_test(i, j) VALUES (4, get_long_string());
+	UPDATE repack_test SET i=3 where i=1;
+
+step check2: 
+	INSERT INTO relfilenodes(node)
+	SELECT c2.relfilenode
+	FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid
+	WHERE c1.relname='repack_test';
+
+	INSERT INTO data_s2(i, j)
+	SELECT i, j FROM repack_test;
+
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT c2.relfilenode
+	FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid
+	WHERE c1.relname='repack_test';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_test;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    4
+(1 row)
+
+count
+-----
+    0
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index fcc85414515..a414abb924b 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -45,6 +45,8 @@ tests += {
     'specs': [
       'basic',
       'inplace',
+      'repack',
+      'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
     ],
diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec
new file mode 100644
index 00000000000..d727a9b056b
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack.spec
@@ -0,0 +1,142 @@
+# REPACK (CONCURRENTLY) ... USING INDEX ...;
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_test(i int PRIMARY KEY, j int);
+	INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_test;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_test USING INDEX repack_test_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_test';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_test ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_test;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step change_existing
+{
+	UPDATE repack_test SET i=10 where i=1;
+	UPDATE repack_test SET j=20 where i=2;
+	UPDATE repack_test SET i=30 where i=3;
+	UPDATE repack_test SET i=40 where i=30;
+	DELETE FROM repack_test WHERE i=4;
+}
+# Insert new rows and UPDATE / DELETE some of them. Again, update both key and
+# non-key column.
+step change_new
+{
+	INSERT INTO repack_test(i, j) VALUES (5, 5), (6, 6), (7, 7), (8, 8);
+	UPDATE repack_test SET i=50 where i=5;
+	UPDATE repack_test SET j=60 where i=6;
+	DELETE FROM repack_test WHERE i=7;
+}
+
+# When applying concurrent data changes, we should see the effects of an
+# in-progress subtransaction.
+#
+# XXX Not sure this test is useful now - it was designed for the patch that
+# preserves tuple visibility and which therefore modifies
+# TransactionIdIsCurrentTransactionId().
+step change_subxact1
+{
+	BEGIN;
+	INSERT INTO repack_test(i, j) VALUES (100, 100);
+	SAVEPOINT s1;
+	UPDATE repack_test SET i=101 where i=100;
+	SAVEPOINT s2;
+	UPDATE repack_test SET i=102 where i=101;
+	COMMIT;
+}
+
+# When applying concurrent data changes, we should not see the effects of a
+# rolled back subtransaction.
+#
+# XXX Is this test useful? See above.
+step change_subxact2
+{
+	BEGIN;
+	SAVEPOINT s1;
+	INSERT INTO repack_test(i, j) VALUES (110, 110);
+	ROLLBACK TO SAVEPOINT s1;
+	INSERT INTO repack_test(i, j) VALUES (110, 111);
+	COMMIT;
+}
+
+# Check the table from the perspective of s2.
+step check2
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_test';
+
+	SELECT i, j FROM repack_test ORDER BY i, j;
+
+	INSERT INTO data_s2(i, j)
+	SELECT i, j FROM repack_test;
+}
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	change_existing
+	change_new
+	change_subxact1
+	change_subxact2
+	check2
+	wakeup_before_lock
+	check1
diff --git a/src/test/modules/injection_points/specs/repack_toast.spec b/src/test/modules/injection_points/specs/repack_toast.spec
new file mode 100644
index 00000000000..b878b198971
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_toast.spec
@@ -0,0 +1,112 @@
+# REPACK (CONCURRENTLY);
+#
+# Test handling of TOAST. At the same time, no tuplesort.
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	-- Return a string that needs to be TOASTed.
+	CREATE FUNCTION get_long_string()
+	RETURNS text
+	LANGUAGE sql as $$
+		SELECT string_agg(chr(65 + trunc(25 * random())::int), '')
+		FROM generate_series(1, 2048) s(x);
+	$$;
+
+	CREATE TABLE repack_test(i int PRIMARY KEY, j text);
+	INSERT INTO repack_test(i, j) VALUES (1, get_long_string()),
+		(2, get_long_string()), (3, get_long_string());
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j text);
+	CREATE TABLE data_s2(i int, j text);
+}
+
+teardown
+{
+	DROP TABLE repack_test;
+	DROP EXTENSION injection_points;
+	DROP FUNCTION get_long_string();
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_test;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT c2.relfilenode
+	FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid
+	WHERE c1.relname='repack_test';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_test;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+    SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+step change
+# Separately test UPDATE of both plain ("i") and TOASTed ("j") attribute. In
+# the first case, the new tuple we get from reorderbuffer.c contains "j" as a
+# TOAST pointer, which we need to update so it points to the new heap. In the
+# latter case, we receive "j" as "external indirect" value - here we test that
+# the decoding worker writes the tuple to a file correctly and that the
+# backend executing REPACK manages to restore it.
+{
+	UPDATE repack_test SET j=get_long_string() where i=2;
+	DELETE FROM repack_test WHERE i=3;
+	INSERT INTO repack_test(i, j) VALUES (4, get_long_string());
+	UPDATE repack_test SET i=3 where i=1;
+}
+# Check the table from the perspective of s2.
+step check2
+{
+	INSERT INTO relfilenodes(node)
+	SELECT c2.relfilenode
+	FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid
+	WHERE c1.relname='repack_test';
+
+	INSERT INTO data_s2(i, j)
+	SELECT i, j FROM repack_test;
+}
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	change
+	check2
+	wakeup_before_lock
+	check1
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..84d61f3ab34 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2021,7 +2021,7 @@ pg_stat_progress_cluster| SELECT pid,
     phase,
     repack_index_relid AS cluster_index_relid,
     heap_tuples_scanned,
-    heap_tuples_written,
+    (heap_tuples_inserted + heap_tuples_updated) AS heap_tuples_written,
     heap_blks_total,
     heap_blks_scanned,
     index_rebuild_count
@@ -2101,17 +2101,20 @@ pg_stat_progress_repack| SELECT s.pid,
             WHEN 2 THEN 'index scanning heap'::text
             WHEN 3 THEN 'sorting tuples'::text
             WHEN 4 THEN 'writing new heap'::text
-            WHEN 5 THEN 'swapping relation files'::text
-            WHEN 6 THEN 'rebuilding index'::text
-            WHEN 7 THEN 'performing final cleanup'::text
+            WHEN 5 THEN 'catch-up'::text
+            WHEN 6 THEN 'swapping relation files'::text
+            WHEN 7 THEN 'rebuilding index'::text
+            WHEN 8 THEN 'performing final cleanup'::text
             ELSE NULL::text
         END AS phase,
     (s.param3)::oid AS repack_index_relid,
     s.param4 AS heap_tuples_scanned,
-    s.param5 AS heap_tuples_written,
-    s.param6 AS heap_blks_total,
-    s.param7 AS heap_blks_scanned,
-    s.param8 AS index_rebuild_count
+    s.param5 AS heap_tuples_inserted,
+    s.param6 AS heap_tuples_updated,
+    s.param7 AS heap_tuples_deleted,
+    s.param8 AS heap_blks_total,
+    s.param9 AS heap_blks_scanned,
+    s.param10 AS index_rebuild_count
    FROM (pg_stat_get_progress_info('REPACK'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
      LEFT JOIN pg_database d ON ((s.datid = d.oid)));
 pg_stat_progress_vacuum| SELECT s.pid,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 712d84128ca..acc2adab534 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -426,6 +426,7 @@ CatCacheHeader
 CatalogId
 CatalogIdMapEntry
 CatalogIndexState
+ChangeContext
 ChangeVarNodes_callback
 ChangeVarNodes_context
 ChannelName
@@ -503,6 +504,7 @@ CompressFileHandle
 CompressionLocation
 CompressorState
 ComputeXidHorizonsResult
+ConcurrentChangeKind
 ConditionVariable
 ConditionVariableMinimallyPadded
 ConditionalStack
@@ -644,6 +646,8 @@ DeclareCursorStmt
 DecodedBkpBlock
 DecodedXLogRecord
 DecodingOutputState
+DecodingWorker
+DecodingWorkerShared
 DefElem
 DefElemAction
 DefaultACLInfo
@@ -1303,6 +1307,7 @@ IndexElem
 IndexFetchHeapData
 IndexFetchTableData
 IndexInfo
+IndexInsertState
 IndexList
 IndexOnlyScan
 IndexOnlyScanState
@@ -2617,6 +2622,7 @@ ReorderBufferTupleCidKey
 ReorderBufferUpdateProgressTxnCB
 ReorderTuple
 RepackCommand
+RepackDecodingState
 RepackStmt
 ReparameterizeForeignPathByChild_function
 ReplOriginId
-- 
2.47.3


--cldir6umqisr673d
Content-Type: text/x-diff; charset=utf-8
Content-Disposition: attachment;
	filename="v45-0004-Use-BulkInsertState-when-copying-data-to-the-new.patch"



^ permalink  raw  reply  [nested|flat] 12+ messages in thread


end of thread, other threads:[~2026-03-11 14:16 UTC | newest]

Thread overview: 12+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2020-09-05 22:10 [PATCH 5/6] BRIN minmax-multi indexes Tomas Vondra <tv@fuzzy.cz>
2020-09-12 13:07 [PATCH 4/5] BRIN minmax-multi indexes Tomas Vondra <tomas.vondra@postgresql.org>
2020-09-12 13:07 [PATCH 5/6] BRIN minmax-multi indexes Tomas Vondra <tomas.vondra@postgresql.org>
2020-09-12 13:07 [PATCH 5/6] BRIN minmax-multi indexes Tomas Vondra <tomas.vondra@postgresql.org>
2020-12-16 20:32 [PATCH 7/8] BRIN minmax-multi indexes Tomas Vondra <tomas.vondra@postgresql.org>
2020-12-16 20:32 [PATCH 5/6] BRIN minmax-multi indexes Tomas Vondra <tomas.vondra@postgresql.org>
2021-02-03 18:00 [PATCH 6/8] BRIN minmax-multi indexes Tomas Vondra <tomas@2ndquadrant.com>
2021-02-03 18:00 [PATCH 5/6] BRIN minmax-multi indexes Tomas Vondra <tomas@2ndquadrant.com>
2021-02-03 18:00 [PATCH 5/9] BRIN minmax-multi indexes Tomas Vondra <tomas@2ndquadrant.com>
2021-02-03 18:00 [PATCH 5/9] BRIN minmax-multi indexes Tomas Vondra <tomas@2ndquadrant.com>
2021-03-21 23:24 [PATCH 6/8] BRIN minmax-multi indexes Tomas Vondra <tomas.vondra@postgresql.org>
2026-03-11 14:16 [PATCH v45 3/8] Add CONCURRENTLY option to REPACK command. Antonin Houska <ah@cybertec.at>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox